From 762a0e5b4714a2e0d33efd4b92f8b39793716c41 Mon Sep 17 00:00:00 2001 From: Alisander Qoshqosh <37006439+qalisander@users.noreply.github.com> Date: Fri, 27 Sep 2024 20:42:34 +0400 Subject: [PATCH] feat: erc165 support interface (#281) Adds erc165 standard altogether with a proc macro, that lets to compute interface id in a simple way. Resolves #33 #### PR Checklist - [x] Tests - [x] Documentation --------- Co-authored-by: Daniel Bigos (cherry picked from commit 9b04143090dd3308bbce92da529e4f81d9f1442d) --- .github/workflows/check-publish.yml | 3 + Cargo.lock | 11 ++ Cargo.toml | 3 + contracts-proc/Cargo.toml | 22 ++++ contracts-proc/src/interface_id.rs | 102 ++++++++++++++++++ contracts-proc/src/lib.rs | 64 +++++++++++ contracts/Cargo.toml | 1 + .../src/token/erc20/extensions/metadata.rs | 24 +++++ contracts/src/token/erc20/mod.rs | 28 ++++- .../src/token/erc721/extensions/enumerable.rs | 23 +++- .../src/token/erc721/extensions/metadata.rs | 26 ++++- contracts/src/token/erc721/mod.rs | 27 ++++- contracts/src/utils/introspection/erc165.rs | 64 +++++++++++ contracts/src/utils/introspection/mod.rs | 2 + contracts/src/utils/mod.rs | 1 + examples/erc20/src/lib.rs | 9 +- examples/erc20/tests/abi/mod.rs | 2 + examples/erc20/tests/erc20.rs | 47 ++++++++ examples/erc721/src/lib.rs | 9 +- examples/erc721/tests/abi/mod.rs | 5 + examples/erc721/tests/erc721.rs | 42 ++++++++ 21 files changed, 506 insertions(+), 9 deletions(-) create mode 100644 contracts-proc/Cargo.toml create mode 100644 contracts-proc/src/interface_id.rs create mode 100644 contracts-proc/src/lib.rs create mode 100644 contracts/src/utils/introspection/erc165.rs create mode 100644 contracts/src/utils/introspection/mod.rs diff --git a/.github/workflows/check-publish.yml b/.github/workflows/check-publish.yml index 67bbb311..132eb00c 100644 --- a/.github/workflows/check-publish.yml +++ b/.github/workflows/check-publish.yml @@ -37,6 +37,9 @@ jobs: - 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 591250ee..e9a680f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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 059b99e0..1bcc000d 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", @@ -98,6 +100,7 @@ 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" } diff --git a/contracts-proc/Cargo.toml b/contracts-proc/Cargo.toml new file mode 100644 index 00000000..763639e8 --- /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 00000000..c2347caf --- /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 { + 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 00000000..0d174638 --- /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>; +/// +/// fn owner_of(&self, token_id: U256) -> Result>; +/// +/// fn safe_transfer_from( +/// &mut self, +/// from: Address, +/// to: Address, +/// token_id: U256, +/// ) -> Result<(), Vec>; +/// +/// #[selector(name = "safeTransferFrom")] +/// fn safe_transfer_from_with_data( +/// &mut self, +/// from: Address, +/// to: Address, +/// token_id: U256, +/// data: Bytes, +/// ) -> Result<(), Vec>; +/// } +/// +/// impl IErc165 for Erc721 { +/// fn supports_interface(interface_id: FixedBytes<4>) -> bool { +/// ::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 f079ce0e..da3bd8f6 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -15,6 +15,7 @@ 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"] } diff --git a/contracts/src/token/erc20/extensions/metadata.rs b/contracts/src/token/erc20/extensions/metadata.rs index 4035f558..ea8680cb 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 { + ::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 = ::INTERFACE_ID; + let expected = 0xa219a025; + assert_eq!(actual, expected); + } +} diff --git a/contracts/src/token/erc20/mod.rs b/contracts/src/token/erc20/mod.rs index 5f9629af..b1b069d5 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! { @@ -111,6 +114,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>; @@ -286,6 +290,13 @@ impl IErc20 for Erc20 { } } +impl IErc165 for Erc20 { + fn supports_interface(interface_id: FixedBytes<4>) -> bool { + ::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. @@ -550,6 +561,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) { @@ -882,4 +897,15 @@ mod tests { let result = contract.approve(Address::ZERO, one); assert!(matches!(result, Err(Error::InvalidSpender(_)))); } + + #[motsu::test] + fn interface_id() { + let actual = ::INTERFACE_ID; + let expected = 0x36372b07; + assert_eq!(actual, expected); + + let actual = ::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 263f676a..2c5095aa 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 { + ::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 = ::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 5b315bf4..634d0eda 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 { + ::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 = ::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 119422f1..c88262b1 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>; @@ -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 { + ::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 = ::INTERFACE_ID; + let expected = 0x80ac58cd; + assert_eq!(actual, expected); + + let actual = ::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 00000000..bace2799 --- /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 00000000..72077bf5 --- /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 7c24bc20..b8f56cef 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/erc20/src/lib.rs b/examples/erc20/src/lib.rs index 407e87d0..ace37b5f 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}; @@ -105,4 +105,9 @@ impl Erc20Example { self.pausable.when_not_paused()?; 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) + } } diff --git a/examples/erc20/tests/abi/mod.rs b/examples/erc20/tests/abi/mod.rs index 0e652d26..f042e396 100644 --- a/examples/erc20/tests/abi/mod.rs +++ b/examples/erc20/tests/abi/mod.rs @@ -29,6 +29,8 @@ sol!( #[derive(Debug)] function whenNotPaused() external view; + 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 cc82c8de..a1301939 100644 --- a/examples/erc20/tests/erc20.rs +++ b/examples/erc20/tests/erc20.rs @@ -1340,3 +1340,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::() + .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/src/lib.rs b/examples/erc721/src/lib.rs index 8d2a4d62..3b1c22ef 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, @@ -151,4 +151,9 @@ impl Erc721Example { Ok(()) } + + pub fn supports_interface(interface_id: FixedBytes<4>) -> bool { + Erc721::supports_interface(interface_id) + || Enumerable::supports_interface(interface_id) + } } diff --git a/examples/erc721/tests/abi/mod.rs b/examples/erc721/tests/abi/mod.rs index b02f4448..796f4174 100644 --- a/examples/erc721/tests/abi/mod.rs +++ b/examples/erc721/tests/abi/mod.rs @@ -18,8 +18,10 @@ 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; @@ -27,11 +29,14 @@ sol!( function whenPaused() external view; #[derive(Debug)] function whenNotPaused() external view; + #[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 b8bf68fd..f00504a9 100644 --- a/examples/erc721/tests/erc721.rs +++ b/examples/erc721/tests/erc721.rs @@ -2097,3 +2097,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(()) +}