Repository: OpenZeppelin/rust-contracts-stylus
Pull request #309 (2024-10-04 08:20:02)

fix(M-01):  Erc165 standard support
Resolves: #319

#### PR Checklist

- [x] Tests
- [x] Documentation


qalisander: Erc165 depends on the new workspace structure. To not have many conflicts I've pulled that commit also.
Second thing @ggonzalez94 is how check link pipeline should work with not published yet changes?

Patch:
diff --git a/.github/workflows/check-publish.yml b/.github/workflows/check-publish.yml
new file mode 100644
index 0000000..132eb00
--- /dev/null
+++ b/.github/workflows/check-publish.yml
@@ -0,0 +1,45 @@
+name: check-publish
+# This workflow checks that the libraries can be published on crates.io.
+permissions:
+  contents: read
+on:
+  push:
+    branches: [ main ]
+  pull_request:
+concurrency:
+  group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+env:
+  CARGO_TERM_COLOR: always
+jobs:
+  check-publish:
+    name: Check publish on crates.io
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: set up rust
+        uses: dtolnay/rust-toolchain@master
+        id: toolchain
+        with:
+          target: wasm32-unknown-unknown
+          components: rust-src
+          toolchain: nightly-2024-01-01
+
+      - uses: Swatinem/rust-cache@v2
+
+      - name: check motsu-proc
+        run: cargo publish -p motsu-proc --dry-run
+
+      - name: check motsu
+        run: cargo publish -p motsu --dry-run
+
+      - name: check openzeppelin-crypto
+        run: cargo publish -p openzeppelin-crypto --target wasm32-unknown-unknown  --dry-run
+
+      - name: check openzeppelin-stylus-proc
+        run: cargo publish -p openzeppelin-stylus-proc --target wasm32-unknown-unknown  --dry-run
+
+        # TODO: https://github.com/OpenZeppelin/rust-contracts-stylus/issues/291
+      # - name: check openzeppelin-stylus
+        # run: cargo publish -p openzeppelin-stylus --target wasm32-unknown-unknown  --dry-run
diff --git a/Cargo.lock b/Cargo.lock
index ebb25bb..e9a680f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,7 +4,7 @@ version = 3
 
 [[package]]
 name = "access-control-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -794,7 +794,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
 [[package]]
 name = "basic-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy-primitives",
  "mini-alloc",
@@ -816,7 +816,7 @@ dependencies = [
 
 [[package]]
 name = "benches"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1262,7 +1262,7 @@ dependencies = [
 
 [[package]]
 name = "cryptography-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1514,7 +1514,7 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "erc20-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1529,7 +1529,7 @@ dependencies = [
 
 [[package]]
 name = "erc20-permit-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1544,7 +1544,7 @@ dependencies = [
 
 [[package]]
 name = "erc721-consecutive-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1561,7 +1561,7 @@ dependencies = [
 
 [[package]]
 name = "erc721-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -1577,7 +1577,7 @@ dependencies = [
 
 [[package]]
 name = "erc721-metadata-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy",
  "alloy-primitives",
@@ -2360,7 +2360,7 @@ checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
 
 [[package]]
 name = "merkle-proofs-example"
-version = "0.0.0"
+version = "0.1.0-rc"
 dependencies = [
  "alloy-primitives",
  "alloy-sol-types",
@@ -2591,11 +2591,22 @@ dependencies = [
  "keccak-const",
  "mini-alloc",
  "motsu",
+ "openzeppelin-stylus-proc",
  "rand",
  "stylus-proc",
  "stylus-sdk",
 ]
 
+[[package]]
+name = "openzeppelin-stylus-proc"
+version = "0.1.0-rc"
+dependencies = [
+ "convert_case 0.6.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.68",
+]
+
 [[package]]
 name = "ownable-example"
 version = "0.0.0"
diff --git a/Cargo.toml b/Cargo.toml
index f196fd1..1bcc000 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,7 @@
 [workspace]
 members = [
   "contracts",
+  "contracts-proc",
   "lib/crypto",
   "lib/motsu",
   "lib/motsu-proc",
@@ -21,6 +22,7 @@ members = [
 ]
 default-members = [
   "contracts",
+  "contracts-proc",
   "lib/crypto",
   "lib/motsu",
   "lib/motsu-proc",
@@ -48,6 +50,7 @@ edition = "2021"
 license = "MIT"
 keywords = ["arbitrum", "ethereum", "stylus"]
 repository = "https://github.com/OpenZeppelin/rust-contracts-stylus"
+version = "0.1.0-rc"
 
 [workspace.lints.rust]
 missing_docs = "warn"
@@ -95,6 +98,15 @@ syn = { version = "2.0.58", features = ["full"] }
 proc-macro2 = "1.0.79"
 quote = "1.0.35"
 
+# members
+openzeppelin-stylus = { path = "contracts" }
+openzeppelin-stylus-proc = { path = "contracts-proc" }
+openzeppelin-crypto = { path = "lib/crypto" }
+motsu = { path = "lib/motsu"}
+motsu-proc = { path = "lib/motsu-proc", version = "0.1.0" }
+e2e = { path = "lib/e2e" }
+e2e-proc = {path = "lib/e2e-proc"}
+
 [profile.release]
 codegen-units = 1
 panic = "abort"
diff --git a/benches/Cargo.toml b/benches/Cargo.toml
index 921de2f..f16effd 100644
--- a/benches/Cargo.toml
+++ b/benches/Cargo.toml
@@ -4,15 +4,15 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives = { workspace = true, features = ["tiny-keccak"] }
 alloy.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 koba.workspace = true
-e2e = { path = "../lib/e2e" }
+e2e.workspace = true
 serde = "1.0.203"
 keccak-const = "0.2.0"
diff --git a/contracts-proc/Cargo.toml b/contracts-proc/Cargo.toml
new file mode 100644
index 0000000..763639e
--- /dev/null
+++ b/contracts-proc/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "openzeppelin-stylus-proc"
+description = "Procedural macros for OpenZeppelin Stylus contracts"
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+keywords.workspace = true
+repository.workspace = true
+
+
+[dependencies]
+proc-macro2.workspace = true
+quote.workspace = true
+syn.workspace = true
+convert_case = "0.6.0"
+
+[lints]
+workspace = true
+
+[lib]
+proc-macro = true
diff --git a/contracts-proc/src/interface_id.rs b/contracts-proc/src/interface_id.rs
new file mode 100644
index 0000000..c2347ca
--- /dev/null
+++ b/contracts-proc/src/interface_id.rs
@@ -0,0 +1,102 @@
+//! Defines the `#[interface_id]` procedural macro.
+
+use std::mem;
+
+use convert_case::{Case, Casing};
+use proc_macro::TokenStream;
+use proc_macro2::Ident;
+use quote::quote;
+use syn::{
+    parse::{Parse, ParseStream},
+    parse_macro_input, FnArg, ItemTrait, LitStr, Result, Token, TraitItem,
+};
+
+/// Computes an interface id as an associated constant for the trait.
+pub(crate) fn interface_id(
+    _attr: &TokenStream,
+    input: TokenStream,
+) -> TokenStream {
+    let mut input = parse_macro_input!(input as ItemTrait);
+
+    let mut selectors = Vec::new();
+    for item in &mut input.items {
+        let TraitItem::Fn(func) = item else {
+            continue;
+        };
+
+        let mut override_fn_name = None;
+        for attr in mem::take(&mut func.attrs) {
+            if attr.path().is_ident("selector") {
+                if override_fn_name.is_some() {
+                    error!(attr.path(), "more than one selector attribute");
+                }
+                let args: SelectorArgs = match attr.parse_args() {
+                    Ok(args) => args,
+                    Err(error) => error!(attr.path(), "{}", error),
+                };
+                override_fn_name = Some(args.name);
+            } else {
+                // Put back any other attributes.
+                func.attrs.push(attr);
+            }
+        }
+
+        let solidity_fn_name = override_fn_name.unwrap_or_else(|| {
+            let rust_fn_name = func.sig.ident.to_string();
+            rust_fn_name.to_case(Case::Camel)
+        });
+
+        let arg_types = func.sig.inputs.iter().filter_map(|arg| match arg {
+            FnArg::Typed(t) => Some(t.ty.clone()),
+            // Opt out any `self` arguments.
+            FnArg::Receiver(_) => None,
+        });
+
+        // Store selector expression from every function in the trait.
+        selectors.push(
+            quote! { u32::from_be_bytes(stylus_sdk::function_selector!(#solidity_fn_name #(, #arg_types )*)) }
+        );
+    }
+
+    let name = input.ident;
+    let vis = input.vis;
+    let attrs = input.attrs;
+    let trait_items = input.items;
+    let (_impl_generics, ty_generics, where_clause) =
+        input.generics.split_for_impl();
+
+    // Keep the same trait with an additional associated constant
+    // `INTERFACE_ID`.
+    quote! {
+        #(#attrs)*
+        #vis trait #name #ty_generics #where_clause {
+            #(#trait_items)*
+
+            #[doc = concat!("Solidity interface id associated with ", stringify!(#name), " trait.")]
+            #[doc = "Computed as a XOR of selectors for each function in the trait."]
+            const INTERFACE_ID: u32 = {
+                #(#selectors)^*
+            };
+        }
+    }
+    .into()
+}
+
+/// Contains arguments of the `#[selector(..)]` attribute.
+struct SelectorArgs {
+    name: String,
+}
+
+impl Parse for SelectorArgs {
+    fn parse(input: ParseStream) -> Result<Self> {
+        let ident: Ident = input.parse()?;
+
+        if ident == "name" {
+            let _: Token![=] = input.parse()?;
+            let lit: LitStr = input.parse()?;
+            Ok(SelectorArgs { name: lit.value() })
+        } else {
+            error!(@ident, "expected identifier 'name'")
+        }
+    }
+}
diff --git a/contracts-proc/src/lib.rs b/contracts-proc/src/lib.rs
new file mode 100644
index 0000000..0d17463
--- /dev/null
+++ b/contracts-proc/src/lib.rs
@@ -0,0 +1,64 @@
+//! Procedural macro definitions used in `openzeppelin-stylus` smart contracts
+//! library.
+
+extern crate proc_macro;
+use proc_macro::TokenStream;
+
+/// Shorthand to print nice errors.
+///
+/// Note that it's defined before the module declarations.
+macro_rules! error {
+    ($tokens:expr, $($msg:expr),+ $(,)?) => {{
+        let error = syn::Error::new(syn::spanned::Spanned::span(&$tokens), format!($($msg),+));
+        return error.to_compile_error().into();
+    }};
+    (@ $tokens:expr, $($msg:expr),+ $(,)?) => {{
+        return Err(syn::Error::new(syn::spanned::Spanned::span(&$tokens), format!($($msg),+)))
+    }};
+}
+
+mod interface_id;
+
+/// Computes the interface id as an associated constant `INTERFACE_ID` for the
+/// trait that describes contract's abi.
+///
+/// Selector collision should be handled with
+/// macro `#[selector(name = "actualSolidityMethodName")]` on top of the method.
+///
+/// # Examples
+///
+/// ```rust,ignore
+/// #[interface_id]
+/// pub trait IErc721 {
+///     fn balance_of(&self, owner: Address) -> Result<U256, Vec<u8>>;
+///
+///     fn owner_of(&self, token_id: U256) -> Result<Address, Vec<u8>>;
+///
+///     fn safe_transfer_from(
+///         &mut self,
+///         from: Address,
+///         to: Address,
+///         token_id: U256,
+///     ) -> Result<(), Vec<u8>>;
+///
+///     #[selector(name = "safeTransferFrom")]
+///     fn safe_transfer_from_with_data(
+///         &mut self,
+///         from: Address,
+///         to: Address,
+///         token_id: U256,
+///         data: Bytes,
+///     ) -> Result<(), Vec<u8>>;
+/// }
+///
+/// impl IErc165 for Erc721 {
+///     fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+///         <Self as IErc721>::INTERFACE_ID == u32::from_be_bytes(*interface_id)
+///             || Erc165::supports_interface(interface_id)
+///     }
+/// }
+/// ```
+#[proc_macro_attribute]
+pub fn interface_id(attr: TokenStream, input: TokenStream) -> TokenStream {
+    interface_id::interface_id(&attr, input)
+}
diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml
index d7c6434..da3bd8f 100644
--- a/contracts/Cargo.toml
+++ b/contracts/Cargo.toml
@@ -6,7 +6,7 @@ edition.workspace = true
 keywords.workspace = true
 license.workspace = true
 repository.workspace = true
-version = "0.1.0-rc"
+version.workspace = true
 
 [dependencies]
 alloy-primitives.workspace = true
@@ -15,10 +15,11 @@ stylus-sdk.workspace = true
 stylus-proc.workspace = true
 mini-alloc.workspace = true
 keccak-const.workspace = true
+openzeppelin-stylus-proc.workspace = true
 
 [dev-dependencies]
 alloy-primitives = { workspace = true, features = ["arbitrary"] }
-motsu = { path = "../lib/motsu" }
+motsu.workspace = true
 rand.workspace = true
 
 [features]
diff --git a/contracts/src/token/erc20/extensions/burnable.rs b/contracts/src/token/erc20/extensions/burnable.rs
index d3e6e4d..fce7fe9 100644
--- a/contracts/src/token/erc20/extensions/burnable.rs
+++ b/contracts/src/token/erc20/extensions/burnable.rs
@@ -158,7 +158,7 @@ mod tests {
     }
 
     #[motsu::test]
-    fn burns_from_errors_when_invalid_sender(contract: Erc20) {
+    fn burns_from_errors_when_invalid_approver(contract: Erc20) {
         let one = uint!(1_U256);
 
         contract
@@ -168,7 +168,7 @@ mod tests {
             .set(one);
 
         let result = contract.burn_from(Address::ZERO, one);
-        assert!(matches!(result, Err(Error::InvalidSender(_))));
+        assert!(matches!(result, Err(Error::InvalidApprover(_))));
     }
 
     #[motsu::test]
diff --git a/contracts/src/token/erc20/extensions/metadata.rs b/contracts/src/token/erc20/extensions/metadata.rs
index 4035f55..ea8680c 100644
--- a/contracts/src/token/erc20/extensions/metadata.rs
+++ b/contracts/src/token/erc20/extensions/metadata.rs
@@ -2,8 +2,12 @@
 
 use alloc::string::String;
 
+use alloy_primitives::FixedBytes;
+use openzeppelin_stylus_proc::interface_id;
 use stylus_proc::{public, sol_storage};
 
+use crate::utils::introspection::erc165::IErc165;
+
 /// Number of decimals used by default on implementors of [`Metadata`].
 pub const DEFAULT_DECIMALS: u8 = 18;
 
@@ -20,6 +24,7 @@ sol_storage! {
 }
 
 /// Interface for the optional metadata functions from the ERC-20 standard.
+#[interface_id]
 pub trait IErc20Metadata {
     /// Returns the name of the token.
     ///
@@ -76,3 +81,22 @@ impl IErc20Metadata for Erc20Metadata {
         DEFAULT_DECIMALS
     }
 }
+
+impl IErc165 for Erc20Metadata {
+    fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+        <Self as IErc20Metadata>::INTERFACE_ID
+            == u32::from_be_bytes(*interface_id)
+    }
+}
+
+#[cfg(all(test, feature = "std"))]
+mod tests {
+    use crate::token::erc20::extensions::{Erc20Metadata, IErc20Metadata};
+
+    #[motsu::test]
+    fn interface_id() {
+        let actual = <Erc20Metadata as IErc20Metadata>::INTERFACE_ID;
+        let expected = 0xa219a025;
+        assert_eq!(actual, expected);
+    }
+}
diff --git a/contracts/src/token/erc20/extensions/permit.rs b/contracts/src/token/erc20/extensions/permit.rs
index 0e57300..1cd597a 100644
--- a/contracts/src/token/erc20/extensions/permit.rs
+++ b/contracts/src/token/erc20/extensions/permit.rs
@@ -173,7 +173,7 @@ impl<T: IEip712 + StorageType> Erc20Permit<T> {
             return Err(ERC2612InvalidSigner { signer, owner }.into());
         }
 
-        self.erc20._approve(owner, spender, value)?;
+        self.erc20._approve(owner, spender, value, true)?;
 
         Ok(())
     }
diff --git a/contracts/src/token/erc20/mod.rs b/contracts/src/token/erc20/mod.rs
index 5f9629a..2a113d7 100644
--- a/contracts/src/token/erc20/mod.rs
+++ b/contracts/src/token/erc20/mod.rs
@@ -4,8 +4,9 @@
 //! revert instead of returning `false` on failure. This behavior is
 //! nonetheless conventional and does not conflict with the expectations of
 //! [`Erc20`] applications.
-use alloy_primitives::{Address, U256};
+use alloy_primitives::{Address, FixedBytes, U256};
 use alloy_sol_types::sol;
+use openzeppelin_stylus_proc::interface_id;
 use stylus_proc::SolidityError;
 use stylus_sdk::{
     call::MethodError,
@@ -13,6 +14,8 @@ use stylus_sdk::{
     stylus_proc::{public, sol_storage},
 };
 
+use crate::utils::introspection::erc165::{Erc165, IErc165};
+
 pub mod extensions;
 
 sol! {
@@ -70,6 +73,14 @@ sol! {
     #[allow(missing_docs)]
     error ERC20InvalidSpender(address spender);
 
+    /// Indicates a failure with the `approver` of a token to be approved. Used in approvals.
+    /// approver Address initiating an approval operation.
+    ///
+    /// * `approver` - Address initiating an approval operation.
+    #[derive(Debug)]
+    #[allow(missing_docs)]
+    error ERC20InvalidApprover(address approver);
+
 }
 
 /// An [`Erc20`] error defined as described in [ERC-6093].
@@ -90,6 +101,9 @@ pub enum Error {
     /// Indicates a failure with the `spender` to be approved. Used in
     /// approvals.
     InvalidSpender(ERC20InvalidSpender),
+    /// Indicates a failure with the `approver` of a token to be approved. Used
+    /// in approvals. approver Address initiating an approval operation.
+    InvalidApprover(ERC20InvalidApprover),
 }
 
 impl MethodError for Error {
@@ -111,6 +125,7 @@ sol_storage! {
 }
 
 /// Required interface of an [`Erc20`] compliant contract.
+#[interface_id]
 pub trait IErc20 {
     /// The error type associated to this ERC-20 trait implementation.
     type Error: Into<alloc::vec::Vec<u8>>;
@@ -270,7 +285,7 @@ impl IErc20 for Erc20 {
         value: U256,
     ) -> Result<bool, Self::Error> {
         let owner = msg::sender();
-        self._approve(owner, spender, value)
+        self._approve(owner, spender, value, true)
     }
 
     fn transfer_from(
@@ -286,6 +301,13 @@ impl IErc20 for Erc20 {
     }
 }
 
+impl IErc165 for Erc20 {
+    fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+        <Self as IErc20>::INTERFACE_ID == u32::from_be_bytes(*interface_id)
+            || Erc165::supports_interface(interface_id)
+    }
+}
+
 impl Erc20 {
     /// Sets a `value` number of tokens as the allowance of `spender` over the
     /// caller's tokens.
@@ -296,6 +318,7 @@ impl Erc20 {
     /// * `&mut self` - Write access to the contract's state.
     /// * `owner` - Account that owns the tokens.
     /// * `spender` - Account that will spend the tokens.
+    /// * `emit_event` - Emit an [`Approval`] event flag.
     ///
     /// # Errors
     ///
@@ -310,7 +333,14 @@ impl Erc20 {
         owner: Address,
         spender: Address,
         value: U256,
+        emit_event: bool,
     ) -> Result<bool, Error> {
+        if owner.is_zero() {
+            return Err(Error::InvalidApprover(ERC20InvalidApprover {
+                approver: Address::ZERO,
+            }));
+        }
+
         if spender.is_zero() {
             return Err(Error::InvalidSpender(ERC20InvalidSpender {
                 spender: Address::ZERO,
@@ -318,7 +348,9 @@ impl Erc20 {
         }
 
         self._allowances.setter(owner).insert(spender, value);
-        evm::log(Approval { owner, spender, value });
+        if emit_event {
+            evm::log(Approval { owner, spender, value });
+        }
         Ok(true)
     }
 
@@ -523,7 +555,7 @@ impl Erc20 {
         spender: Address,
         value: U256,
     ) -> Result<(), Error> {
-        let current_allowance = self._allowances.get(owner).get(spender);
+        let current_allowance = self.allowance(owner, spender);
         if current_allowance != U256::MAX {
             if current_allowance < value {
                 return Err(Error::InsufficientAllowance(
@@ -535,9 +567,7 @@ impl Erc20 {
                 ));
             }
 
-            self._allowances
-                .setter(owner)
-                .insert(spender, current_allowance - value);
+            self._approve(owner, spender, current_allowance - value, false)?;
         }
 
         Ok(())
@@ -550,6 +580,10 @@ mod tests {
     use stylus_sdk::msg;
 
     use super::{Erc20, Error, IErc20};
+    use crate::{
+        token::erc721::{Erc721, IErc721},
+        utils::introspection::erc165::IErc165,
+    };
 
     #[motsu::test]
     fn reads_balance(contract: Erc20) {
@@ -816,7 +850,7 @@ mod tests {
     }
 
     #[motsu::test]
-    fn transfer_from_errors_when_invalid_sender(contract: Erc20) {
+    fn transfer_from_errors_when_invalid_approver(contract: Erc20) {
         let alice = address!("A11CEacF9aa32246d767FCCD72e02d6bCbcC375d");
         let one = uint!(1_U256);
         contract
@@ -825,7 +859,7 @@ mod tests {
             .setter(msg::sender())
             .set(one);
         let result = contract.transfer_from(Address::ZERO, alice, one);
-        assert!(matches!(result, Err(Error::InvalidSender(_))));
+        assert!(matches!(result, Err(Error::InvalidApprover(_))));
     }
 
     #[motsu::test]
@@ -882,4 +916,15 @@ mod tests {
         let result = contract.approve(Address::ZERO, one);
         assert!(matches!(result, Err(Error::InvalidSpender(_))));
     }
+
+    #[motsu::test]
+    fn interface_id() {
+        let actual = <Erc20 as IErc20>::INTERFACE_ID;
+        let expected = 0x36372b07;
+        assert_eq!(actual, expected);
+
+        let actual = <Erc20 as IErc165>::INTERFACE_ID;
+        let expected = 0x01ffc9a7;
+        assert_eq!(actual, expected);
+    }
 }
diff --git a/contracts/src/token/erc721/extensions/enumerable.rs b/contracts/src/token/erc721/extensions/enumerable.rs
index 263f676..2c5095a 100644
--- a/contracts/src/token/erc721/extensions/enumerable.rs
+++ b/contracts/src/token/erc721/extensions/enumerable.rs
@@ -10,11 +10,15 @@
 //! [`Erc721Enumerable`].
 // TODO: Add link for `Erc721Consecutive` to module docs.
 
-use alloy_primitives::{uint, Address, U256};
+use alloy_primitives::{uint, Address, FixedBytes, U256};
 use alloy_sol_types::sol;
+use openzeppelin_stylus_proc::interface_id;
 use stylus_proc::{public, sol_storage, SolidityError};
 
-use crate::token::{erc721, erc721::IErc721};
+use crate::{
+    token::{erc721, erc721::IErc721},
+    utils::introspection::erc165::IErc165,
+};
 
 sol! {
     /// Indicates an error when an `owner`'s token query
@@ -63,6 +67,7 @@ sol_storage! {
 
 /// This is the interface of the optional `Enumerable` extension
 /// of the ERC-721 standard.
+#[interface_id]
 pub trait IErc721Enumerable {
     /// The error type associated to this ERC-721 enumerable trait
     /// implementation.
@@ -144,6 +149,13 @@ impl IErc721Enumerable for Erc721Enumerable {
     }
 }
 
+impl IErc165 for Erc721Enumerable {
+    fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+        <Self as IErc721Enumerable>::INTERFACE_ID
+            == u32::from_be_bytes(*interface_id)
+    }
+}
+
 impl Erc721Enumerable {
     /// Function to add a token to this extension's
     /// ownership-tracking data structures.
@@ -547,4 +559,11 @@ mod tests {
             contract.token_of_owner_by_index(alice, U256::ZERO).unwrap_err();
         assert!(matches!(err, Error::OutOfBoundsIndex(_)));
     }
+
+    #[motsu::test]
+    fn interface_id() {
+        let actual = <Erc721Enumerable as IErc721Enumerable>::INTERFACE_ID;
+        let expected = 0x780e9d63;
+        assert_eq!(actual, expected);
+    }
 }
diff --git a/contracts/src/token/erc721/extensions/metadata.rs b/contracts/src/token/erc721/extensions/metadata.rs
index 5b315bf..634d0ed 100644
--- a/contracts/src/token/erc721/extensions/metadata.rs
+++ b/contracts/src/token/erc721/extensions/metadata.rs
@@ -2,9 +2,11 @@
 
 use alloc::string::String;
 
+use alloy_primitives::FixedBytes;
+use openzeppelin_stylus_proc::interface_id;
 use stylus_proc::{public, sol_storage};
 
-use crate::utils::Metadata;
+use crate::utils::{introspection::erc165::IErc165, Metadata};
 
 sol_storage! {
     /// Metadata of an [`crate::token::erc721::Erc721`] token.
@@ -17,6 +19,7 @@ sol_storage! {
 }
 
 /// Interface for the optional metadata functions from the ERC-721 standard.
+#[interface_id]
 pub trait IErc721Metadata {
     /// Returns the token collection name.
     ///
@@ -58,3 +61,24 @@ impl IErc721Metadata for Erc721Metadata {
         self._base_uri.get_string()
     }
 }
+
+impl IErc165 for Erc721Metadata {
+    fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+        <Self as IErc721Metadata>::INTERFACE_ID
+            == u32::from_be_bytes(*interface_id)
+    }
+}
+
+#[cfg(all(test, feature = "std"))]
+mod tests {
+    // use crate::token::erc721::extensions::{Erc721Metadata, IErc721Metadata};
+
+    // TODO: IErc721Metadata should be refactored to have same api as solidity
+    //  has:  https://github.com/OpenZeppelin/openzeppelin-contracts/blob/4764ea50750d8bda9096e833706beba86918b163/contracts/token/ERC721/extensions/IERC721Metadata.sol#L12
+    // [motsu::test]
+    // fn interface_id() {
+    //     let actual = <Erc721Metadata as IErc721Metadata>::INTERFACE_ID;
+    //     let expected = 0x5b5e139f;
+    //     assert_eq!(actual, expected);
+    // }
+}
diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs
index 119422f..c88262b 100644
--- a/contracts/src/token/erc721/mod.rs
+++ b/contracts/src/token/erc721/mod.rs
@@ -2,6 +2,7 @@
 use alloc::vec;
 
 use alloy_primitives::{fixed_bytes, uint, Address, FixedBytes, U128, U256};
+use openzeppelin_stylus_proc::interface_id;
 use stylus_sdk::{
     abi::Bytes,
     alloy_sol_types::sol,
@@ -10,7 +11,10 @@ use stylus_sdk::{
     prelude::*,
 };
 
-use crate::utils::math::storage::{AddAssignUnchecked, SubAssignUnchecked};
+use crate::utils::{
+    introspection::erc165::{Erc165, IErc165},
+    math::storage::{AddAssignUnchecked, SubAssignUnchecked},
+};
 
 pub mod extensions;
 
@@ -198,6 +202,7 @@ sol_storage! {
 unsafe impl TopLevelStorage for Erc721 {}
 
 /// Required interface of an [`Erc721`] compliant contract.
+#[interface_id]
 pub trait IErc721 {
     /// The error type associated to this ERC-721 trait implementation.
     type Error: Into<alloc::vec::Vec<u8>>;
@@ -317,6 +322,7 @@ pub trait IErc721 {
     /// # Events
     ///
     /// Emits a [`Transfer`] event.
+    #[selector(name = "safeTransferFrom")]
     fn safe_transfer_from_with_data(
         &mut self,
         from: Address,
@@ -549,6 +555,13 @@ impl IErc721 for Erc721 {
     }
 }
 
+impl IErc165 for Erc721 {
+    fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+        <Self as IErc721>::INTERFACE_ID == u32::from_be_bytes(*interface_id)
+            || Erc165::supports_interface(interface_id)
+    }
+}
+
 impl Erc721 {
     /// Returns the owner of the `token_id`. Does NOT revert if the token
     /// doesn't exist.
@@ -1153,6 +1166,7 @@ mod tests {
         ERC721InvalidReceiver, ERC721InvalidSender, ERC721NonexistentToken,
         Erc721, Error, IErc721,
     };
+    use crate::utils::introspection::erc165::IErc165;
 
     const BOB: Address = address!("F4EaCDAbEf3c8f1EdE91b6f2A6840bc2E4DD3526");
     const DAVE: Address = address!("0BB78F7e7132d1651B4Fd884B7624394e92156F1");
@@ -2487,4 +2501,15 @@ mod tests {
             }) if token_id == t_id
         ));
     }
+
+    #[motsu::test]
+    fn interface_id() {
+        let actual = <Erc721 as IErc721>::INTERFACE_ID;
+        let expected = 0x80ac58cd;
+        assert_eq!(actual, expected);
+
+        let actual = <Erc721 as IErc165>::INTERFACE_ID;
+        let expected = 0x01ffc9a7;
+        assert_eq!(actual, expected);
+    }
 }
diff --git a/contracts/src/utils/introspection/erc165.rs b/contracts/src/utils/introspection/erc165.rs
new file mode 100644
index 0000000..bace279
--- /dev/null
+++ b/contracts/src/utils/introspection/erc165.rs
@@ -0,0 +1,64 @@
+//! Trait and implementation of the ERC-165 standard, as defined in the [ERC].
+//!
+//! [ERC]: https://eips.ethereum.org/EIPS/eip-165
+
+use alloy_primitives::FixedBytes;
+use openzeppelin_stylus_proc::interface_id;
+
+/// Interface of the ERC-165 standard, as defined in the [ERC].
+///
+/// Implementers can declare support of contract interfaces, which others can
+/// query.
+///
+/// For an implementation, see [`Erc165`].
+///
+/// [ERC]: https://eips.ethereum.org/EIPS/eip-165
+#[interface_id]
+pub trait IErc165 {
+    /// Returns true if this contract implements the interface defined by
+    /// `interface_id`. See the corresponding [ERC section]
+    /// to learn more about how these ids are created.
+    ///
+    /// Method [`IErc165::supports_interface`] should be reexported with
+    /// `#[public]` macro manually like this:
+    ///
+    /// ```rust,ignore
+    /// #[public]
+    /// impl Erc20Example {
+    ///     fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+    ///         Erc20::supports_interface(interface_id)
+    ///             || Erc20Metadata::supports_interface(interface_id)
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// # Arguments
+    ///
+    /// * `interface_id` - The interface identifier, as specified in [ERC
+    ///   section]
+    ///
+    /// [ERC section]: https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified
+    fn supports_interface(interface_id: FixedBytes<4>) -> bool;
+}
+
+/// Implementation of the [`IErc165`] trait.
+///
+/// Contracts that want to support ERC-165 should implement the [`IErc165`]
+/// trait for the additional interface id that will be supported and call
+/// [`Erc165::supports_interface`] like:
+///
+/// ```rust,ignore
+/// impl IErc165 for Erc20 {
+///     fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+///         crate::token::erc20::INTERFACE_ID == u32::from_be_bytes(*interface_id)
+///             || Erc165::supports_interface(interface_id)
+///     }
+/// }
+/// ```
+pub struct Erc165;
+
+impl IErc165 for Erc165 {
+    fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+        Self::INTERFACE_ID == u32::from_be_bytes(*interface_id)
+    }
+}
diff --git a/contracts/src/utils/introspection/mod.rs b/contracts/src/utils/introspection/mod.rs
new file mode 100644
index 0000000..72077bf
--- /dev/null
+++ b/contracts/src/utils/introspection/mod.rs
@@ -0,0 +1,2 @@
+//! Stylus contract's introspection helpers library.
+pub mod erc165;
diff --git a/contracts/src/utils/mod.rs b/contracts/src/utils/mod.rs
index 7c24bc2..b8f56ce 100644
--- a/contracts/src/utils/mod.rs
+++ b/contracts/src/utils/mod.rs
@@ -1,5 +1,6 @@
 //! Common Smart Contracts utilities.
 pub mod cryptography;
+pub mod introspection;
 pub mod math;
 pub mod metadata;
 pub mod nonces;
diff --git a/examples/access-control/Cargo.toml b/examples/access-control/Cargo.toml
index d3fdeef..cf03218 100644
--- a/examples/access-control/Cargo.toml
+++ b/examples/access-control/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -15,7 +15,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 
diff --git a/examples/basic/token/Cargo.toml b/examples/basic/token/Cargo.toml
index baee128..077980f 100644
--- a/examples/basic/token/Cargo.toml
+++ b/examples/basic/token/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
diff --git a/examples/ecdsa/Cargo.toml b/examples/ecdsa/Cargo.toml
index 2d5b3a9..7cac7e5 100644
--- a/examples/ecdsa/Cargo.toml
+++ b/examples/ecdsa/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -15,7 +15,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 
diff --git a/examples/erc20-permit/Cargo.toml b/examples/erc20-permit/Cargo.toml
index 29cc342..9008195 100644
--- a/examples/erc20-permit/Cargo.toml
+++ b/examples/erc20-permit/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives = { workspace = true, features = ["tiny-keccak"] }
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -17,7 +17,7 @@ mini-alloc.workspace = true
 alloy.workspace = true
 eyre.workspace = true
 tokio.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 
 [features]
 e2e = []
diff --git a/examples/erc20/Cargo.toml b/examples/erc20/Cargo.toml
index 1815680..037d3be 100644
--- a/examples/erc20/Cargo.toml
+++ b/examples/erc20/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -17,7 +17,7 @@ mini-alloc.workspace = true
 alloy.workspace = true
 eyre.workspace = true
 tokio.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 
 [features]
 e2e = []
diff --git a/examples/erc20/src/lib.rs b/examples/erc20/src/lib.rs
index 4e64a6e..4d8ab3e 100644
--- a/examples/erc20/src/lib.rs
+++ b/examples/erc20/src/lib.rs
@@ -3,13 +3,13 @@ extern crate alloc;
 
 use alloc::vec::Vec;
 
-use alloy_primitives::{Address, U256};
+use alloy_primitives::{Address, FixedBytes, U256};
 use openzeppelin_stylus::{
     token::erc20::{
         extensions::{capped, Capped, Erc20Metadata, IErc20Burnable},
         Erc20, IErc20,
     },
-    utils::Pausable,
+    utils::{introspection::erc165::IErc165, Pausable},
 };
 use stylus_sdk::prelude::{entrypoint, public, sol_storage};
 
@@ -106,6 +106,11 @@ impl Erc20Example {
         self.erc20.transfer_from(from, to, value).map_err(|e| e.into())
     }
 
+    fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+        Erc20::supports_interface(interface_id)
+            || Erc20Metadata::supports_interface(interface_id)
+    }
+
     pub fn pause(&mut self) -> Result<(), Vec<u8>> {
         self.pausable.pause().map_err(|e| e.into())
     }
diff --git a/examples/erc721-consecutive/Cargo.toml b/examples/erc721-consecutive/Cargo.toml
index 2c6eef2..08b7f5b 100644
--- a/examples/erc721-consecutive/Cargo.toml
+++ b/examples/erc721-consecutive/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 alloy-sol-types.workspace = true
 stylus-sdk.workspace = true
@@ -16,7 +16,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 rand.workspace = true
diff --git a/examples/erc721-metadata/Cargo.toml b/examples/erc721-metadata/Cargo.toml
index 1bc6050..62b1f29 100644
--- a/examples/erc721-metadata/Cargo.toml
+++ b/examples/erc721-metadata/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -15,7 +15,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 rand.workspace = true
diff --git a/examples/erc721/Cargo.toml b/examples/erc721/Cargo.toml
index 6f70bb7..c11254d 100644
--- a/examples/erc721/Cargo.toml
+++ b/examples/erc721/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -15,7 +15,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 rand.workspace = true
diff --git a/examples/erc721/src/lib.rs b/examples/erc721/src/lib.rs
index 695f4fb..d77d991 100644
--- a/examples/erc721/src/lib.rs
+++ b/examples/erc721/src/lib.rs
@@ -3,13 +3,13 @@ extern crate alloc;
 
 use alloc::vec::Vec;
 
-use alloy_primitives::{Address, U256};
+use alloy_primitives::{Address, FixedBytes, U256};
 use openzeppelin_stylus::{
     token::erc721::{
         extensions::{Erc721Enumerable as Enumerable, IErc721Burnable},
         Erc721, IErc721,
     },
-    utils::Pausable,
+    utils::{introspection::erc165::IErc165, Pausable},
 };
 use stylus_sdk::{
     abi::Bytes,
@@ -152,6 +152,11 @@ impl Erc721Example {
         Ok(())
     }
 
+    pub fn supports_interface(interface_id: FixedBytes<4>) -> bool {
+        Erc721::supports_interface(interface_id)
+            || Enumerable::supports_interface(interface_id)
+    }
+
     pub fn pause(&mut self) -> Result<(), Vec<u8>> {
         self.pausable.pause().map_err(|e| e.into())
     }
diff --git a/examples/merkle-proofs/Cargo.toml b/examples/merkle-proofs/Cargo.toml
index 60931af..e741ccb 100644
--- a/examples/merkle-proofs/Cargo.toml
+++ b/examples/merkle-proofs/Cargo.toml
@@ -4,10 +4,10 @@ edition.workspace = true
 license.workspace = true
 repository.workspace = true
 publish = false
-version = "0.0.0"
+version.workspace = true
 
 [dependencies]
-openzeppelin-crypto = { path = "../../lib/crypto" }
+openzeppelin-crypto.workspace = true
 alloy-primitives.workspace = true
 alloy-sol-types.workspace = true
 stylus-sdk.workspace = true
diff --git a/examples/ownable/Cargo.toml b/examples/ownable/Cargo.toml
index 13ebcbe..6ac97fa 100644
--- a/examples/ownable/Cargo.toml
+++ b/examples/ownable/Cargo.toml
@@ -7,7 +7,7 @@ publish = false
 version = "0.0.0"
 
 [dependencies]
-openzeppelin-stylus = { path = "../../contracts" }
+openzeppelin-stylus.workspace = true
 alloy-primitives.workspace = true
 stylus-sdk.workspace = true
 stylus-proc.workspace = true
@@ -15,7 +15,7 @@ mini-alloc.workspace = true
 
 [dev-dependencies]
 alloy.workspace = true
-e2e = { path = "../../lib/e2e" }
+e2e.workspace = true
 tokio.workspace = true
 eyre.workspace = true
 
diff --git a/lib/crypto/Cargo.toml b/lib/crypto/Cargo.toml
index cf119fc..81a44fd 100644
--- a/lib/crypto/Cargo.toml
+++ b/lib/crypto/Cargo.toml
@@ -6,7 +6,7 @@ edition.workspace = true
 keywords.workspace = true
 license.workspace = true
 repository.workspace = true
-version = "0.1.0-rc"
+version.workspace = true
 
 [dependencies]
 mini-alloc.workspace = true
diff --git a/lib/motsu-proc/Cargo.toml b/lib/motsu-proc/Cargo.toml
index 1e6bbbc..7b172b6 100644
--- a/lib/motsu-proc/Cargo.toml
+++ b/lib/motsu-proc/Cargo.toml
@@ -13,7 +13,7 @@ quote.workspace = true
 syn.workspace = true
 
 [dev-dependencies]
-motsu = { path = "../motsu" }
+motsu.workspace = true
 alloy-primitives.workspace = true
 alloy-sol-types.workspace = true
 stylus-sdk.workspace = true
diff --git a/lib/motsu/Cargo.toml b/lib/motsu/Cargo.toml
index ed66709..57f1f19 100644
--- a/lib/motsu/Cargo.toml
+++ b/lib/motsu/Cargo.toml
@@ -12,7 +12,7 @@ const-hex.workspace = true
 once_cell.workspace = true
 tiny-keccak.workspace = true
 stylus-sdk.workspace = true
-motsu-proc = { path = "../motsu-proc" }
+motsu-proc.workspace = true
 
 [lints]
 workspace = true


Test patch:
diff --git a/examples/erc20/tests/abi/mod.rs b/examples/erc20/tests/abi/mod.rs
index 9b7405f..eb83535 100644
--- a/examples/erc20/tests/abi/mod.rs
+++ b/examples/erc20/tests/abi/mod.rs
@@ -25,6 +25,8 @@ sol!(
         function pause() external;
         function unpause() external;
 
+        function supportsInterface(bytes4 interface_id) external view returns (bool supportsInterface);
+
         error EnforcedPause();
         error ExpectedPause();
 
diff --git a/examples/erc20/tests/erc20.rs b/examples/erc20/tests/erc20.rs
index a94b499..a61393c 100644
--- a/examples/erc20/tests/erc20.rs
+++ b/examples/erc20/tests/erc20.rs
@@ -1316,3 +1316,50 @@ async fn error_when_transfer_from(alice: Account, bob: Account) -> Result<()> {
 
     Ok(())
 }
+
+// ============================================================================
+// Integration Tests: ERC-165 Support Interface
+// ============================================================================
+
+#[e2e::test]
+async fn support_interface(alice: Account) -> Result<()> {
+    let contract_addr = alice
+        .as_deployer()
+        .with_default_constructor::<constructorCall>()
+        .deploy()
+        .await?
+        .address()?;
+    let contract = Erc20::new(contract_addr, &alice.wallet);
+    let invalid_interface_id: u32 = 0xffffffff;
+    let Erc20::supportsInterfaceReturn {
+        supportsInterface: supports_interface,
+    } = contract.supportsInterface(invalid_interface_id.into()).call().await?;
+
+    assert_eq!(supports_interface, false);
+
+    let erc20_interface_id: u32 = 0x36372b07;
+    let Erc20::supportsInterfaceReturn {
+        supportsInterface: supports_interface,
+    } = contract.supportsInterface(erc20_interface_id.into()).call().await?;
+
+    assert_eq!(supports_interface, true);
+
+    let erc165_interface_id: u32 = 0x01ffc9a7;
+    let Erc20::supportsInterfaceReturn {
+        supportsInterface: supports_interface,
+    } = contract.supportsInterface(erc165_interface_id.into()).call().await?;
+
+    assert_eq!(supports_interface, true);
+
+    let erc20_metadata_interface_id: u32 = 0xa219a025;
+    let Erc20::supportsInterfaceReturn {
+        supportsInterface: supports_interface,
+    } = contract
+        .supportsInterface(erc20_metadata_interface_id.into())
+        .call()
+        .await?;
+
+    assert_eq!(supports_interface, true);
+
+    Ok(())
+}
diff --git a/examples/erc721/tests/abi/mod.rs b/examples/erc721/tests/abi/mod.rs
index 929689b..5d08079 100644
--- a/examples/erc721/tests/abi/mod.rs
+++ b/examples/erc721/tests/abi/mod.rs
@@ -18,16 +18,21 @@ sol!(
         function setApprovalForAll(address operator, bool approved) external;
         function totalSupply() external view returns (uint256 totalSupply);
         function transferFrom(address from, address to, uint256 tokenId) external;
+
         function mint(address to, uint256 tokenId) external;
         function burn(uint256 tokenId) external;
+
         function paused() external view returns (bool paused);
         function pause() external;
         function unpause() external;
+
         #[derive(Debug)]
         function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256 tokenId);
         #[derive(Debug)]
         function tokenByIndex(uint256 index) external view returns (uint256 tokenId);
 
+        function supportsInterface(bytes4 interface_id) external view returns (bool supportsInterface);
+
         error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);
         error ERC721InsufficientApproval(address operator, uint256 tokenId);
         error ERC721InvalidApprover(address approver);
diff --git a/examples/erc721/tests/erc721.rs b/examples/erc721/tests/erc721.rs
index fbd522d..8823e1b 100644
--- a/examples/erc721/tests/erc721.rs
+++ b/examples/erc721/tests/erc721.rs
@@ -2073,3 +2073,45 @@ async fn token_by_index_after_burn_and_some_mints(
 
     Ok(())
 }
+
+// ============================================================================
+// Integration Tests: ERC-165 Support Interface
+// ============================================================================
+
+#[e2e::test]
+async fn support_interface(alice: Account) -> eyre::Result<()> {
+    let contract_addr = alice.as_deployer().deploy().await?.address()?;
+    let contract = Erc721::new(contract_addr, &alice.wallet);
+    let invalid_interface_id: u32 = 0x_ffffffff;
+    let Erc721::supportsInterfaceReturn {
+        supportsInterface: supports_interface,
+    } = contract.supportsInterface(invalid_interface_id.into()).call().await?;
+
+    assert_eq!(supports_interface, false);
+
+    let erc721_interface_id: u32 = 0x80ac58cd;
+    let Erc721::supportsInterfaceReturn {
+        supportsInterface: supports_interface,
+    } = contract.supportsInterface(erc721_interface_id.into()).call().await?;
+
+    assert_eq!(supports_interface, true);
+
+    let erc165_interface_id: u32 = 0x01ffc9a7;
+    let Erc721::supportsInterfaceReturn {
+        supportsInterface: supports_interface,
+    } = contract.supportsInterface(erc165_interface_id.into()).call().await?;
+
+    assert_eq!(supports_interface, true);
+
+    let erc721_enumerable_interface_id: u32 = 0x780e9d63;
+    let Erc721::supportsInterfaceReturn {
+        supportsInterface: supports_interface,
+    } = contract
+        .supportsInterface(erc721_enumerable_interface_id.into())
+        .call()
+        .await?;
+
+    assert_eq!(supports_interface, true);
+
+    Ok(())
+}
diff --git a/lib/e2e/Cargo.toml b/lib/e2e/Cargo.toml
index 6d6cf93..0c48aa1 100644
--- a/lib/e2e/Cargo.toml
+++ b/lib/e2e/Cargo.toml
@@ -15,7 +15,7 @@ eyre.workspace = true
 regex.workspace = true
 once_cell.workspace = true
 koba.workspace = true
-e2e-proc = { path = "../e2e-proc" }
+e2e-proc.workspace = true
 toml = "0.8.13"
 
 [lints]