diff --git a/contracts-proc/src/interface.rs b/contracts-proc/src/interface_id.rs similarity index 96% rename from contracts-proc/src/interface.rs rename to contracts-proc/src/interface_id.rs index 6fa40c4b..d791dde7 100644 --- a/contracts-proc/src/interface.rs +++ b/contracts-proc/src/interface_id.rs @@ -10,7 +10,10 @@ use syn::{ }; /// Computes interface id as an associated constant for the trait. -pub(crate) fn interface(_attr: TokenStream, input: TokenStream) -> TokenStream { +pub(crate) fn interface_id( + _attr: &TokenStream, + input: TokenStream, +) -> TokenStream { let mut input = parse_macro_input!(input as ItemTrait); let mut output = quote! {}; @@ -48,7 +51,7 @@ pub(crate) fn interface(_attr: TokenStream, input: TokenStream) -> TokenStream { let arg_types: Vec<_> = args .filter_map(|arg| match arg { FnArg::Typed(t) => Some(t.ty.clone()), - _ => None, + FnArg::Receiver(_) => None, }) .collect(); diff --git a/contracts-proc/src/lib.rs b/contracts-proc/src/lib.rs index 6060d236..470488d3 100644 --- a/contracts-proc/src/lib.rs +++ b/contracts-proc/src/lib.rs @@ -1,9 +1,12 @@ -// TODO#q: add crate documentation. +//! 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),+)); @@ -14,10 +17,47 @@ macro_rules! error { }}; } -mod interface; +mod interface_id; -/// Computes interface id as an associated constant for the trait. +/// Computes 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<(), alloc::vec::Vec>; +/// +/// #[selector(name = "safeTransferFrom")] +/// fn safe_transfer_from_with_data( +/// &mut self, +/// from: Address, +/// to: Address, +/// token_id: U256, +/// data: Bytes, +/// ) -> Result<(), alloc::vec::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(attr: TokenStream, input: TokenStream) -> TokenStream { - interface::interface(attr, input) +pub fn interface_id(attr: TokenStream, input: TokenStream) -> TokenStream { + interface_id::interface_id(&attr, input) } diff --git a/contracts/src/token/erc20/extensions/metadata.rs b/contracts/src/token/erc20/extensions/metadata.rs index 4035f558..1ab47a2d 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,10 @@ 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) + } +} diff --git a/contracts/src/token/erc20/mod.rs b/contracts/src/token/erc20/mod.rs index 33a9fa29..b1b069d5 100644 --- a/contracts/src/token/erc20/mod.rs +++ b/contracts/src/token/erc20/mod.rs @@ -4,9 +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; +use openzeppelin_stylus_proc::interface_id; use stylus_proc::SolidityError; use stylus_sdk::{ call::MethodError, @@ -14,6 +14,8 @@ use stylus_sdk::{ stylus_proc::{public, sol_storage}, }; +use crate::utils::introspection::erc165::{Erc165, IErc165}; + pub mod extensions; sol! { @@ -112,7 +114,7 @@ sol_storage! { } /// Required interface of an [`Erc20`] compliant contract. -#[interface] +#[interface_id] pub trait IErc20 { /// The error type associated to this ERC-20 trait implementation. type Error: Into>; @@ -288,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. @@ -552,7 +561,10 @@ mod tests { use stylus_sdk::msg; use super::{Erc20, Error, IErc20}; - use crate::token::erc721::{Erc721, IErc721}; + use crate::{ + token::erc721::{Erc721, IErc721}, + utils::introspection::erc165::IErc165, + }; #[motsu::test] fn reads_balance(contract: Erc20) { @@ -889,7 +901,11 @@ mod tests { #[motsu::test] fn interface_id() { let actual = ::INTERFACE_ID; - let expected = 0x_36372b07; + 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..6bfa3a73 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. diff --git a/contracts/src/token/erc721/extensions/metadata.rs b/contracts/src/token/erc721/extensions/metadata.rs index 5b315bf4..3e20bfdc 100644 --- a/contracts/src/token/erc721/extensions/metadata.rs +++ b/contracts/src/token/erc721/extensions/metadata.rs @@ -2,9 +2,14 @@ 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::{ + token::erc20::extensions::IErc20Metadata, + utils::{introspection::erc165::IErc165, Metadata}, +}; sol_storage! { /// Metadata of an [`crate::token::erc721::Erc721`] token. @@ -17,6 +22,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 +64,10 @@ 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) + } +} diff --git a/contracts/src/token/erc721/mod.rs b/contracts/src/token/erc721/mod.rs index 91d45eb6..c88262b1 100644 --- a/contracts/src/token/erc721/mod.rs +++ b/contracts/src/token/erc721/mod.rs @@ -2,7 +2,7 @@ use alloc::vec; use alloy_primitives::{fixed_bytes, uint, Address, FixedBytes, U128, U256}; -use openzeppelin_stylus_proc::interface; +use openzeppelin_stylus_proc::interface_id; use stylus_sdk::{ abi::Bytes, alloy_sol_types::sol, @@ -11,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; @@ -199,7 +202,7 @@ sol_storage! { unsafe impl TopLevelStorage for Erc721 {} /// Required interface of an [`Erc721`] compliant contract. -#[interface] +#[interface_id] pub trait IErc721 { /// The error type associated to this ERC-721 trait implementation. type Error: Into>; @@ -552,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. @@ -1156,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"); @@ -2494,7 +2505,11 @@ mod tests { #[motsu::test] fn interface_id() { let actual = ::INTERFACE_ID; - let expected = 0x_80ac58cd; + 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..38a8401b --- /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 + /// `interfaceId`. 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 8c732c6f..ace37b5f 100644 --- a/examples/erc20/src/lib.rs +++ b/examples/erc20/src/lib.rs @@ -9,7 +9,7 @@ use openzeppelin_stylus::{ extensions::{capped, Capped, Erc20Metadata, IErc20Burnable}, Erc20, IErc20, }, - utils::Pausable, + utils::{introspection::erc165::IErc165, Pausable}, }; use stylus_sdk::prelude::{entrypoint, public, sol_storage}; @@ -106,11 +106,8 @@ impl Erc20Example { self.erc20.transfer_from(from, to, value).map_err(|e| e.into()) } - fn supports_interface( - interface_id: FixedBytes<4>, - ) -> Result> { - let interface_id = u32::from_be_bytes(*interface_id); - let supported = interface_id == ::INTERFACE_ID; - Ok(supported) + fn supports_interface(interface_id: FixedBytes<4>) -> bool { + Erc20::supports_interface(interface_id) + || Erc20Metadata::supports_interface(interface_id) } } diff --git a/examples/erc20/tests/erc20.rs b/examples/erc20/tests/erc20.rs index e6595f00..68cb443c 100644 --- a/examples/erc20/tests/erc20.rs +++ b/examples/erc20/tests/erc20.rs @@ -1350,17 +1350,24 @@ async fn support_interface(alice: Account) -> Result<()> { .await? .address()?; let contract = Erc20::new(contract_addr, &alice.wallet); - let invalid_interface_id: u32 = 0x_ffffffff; + 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 valid_interface_id: u32 = 0x_36372b07; + let erc20_interface_id: u32 = 0x36372b07; let Erc20::supportsInterfaceReturn { supportsInterface: supports_interface, - } = contract.supportsInterface(valid_interface_id.into()).call().await?; + } = 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); diff --git a/examples/erc721/src/lib.rs b/examples/erc721/src/lib.rs index a707e070..3b1c22ef 100644 --- a/examples/erc721/src/lib.rs +++ b/examples/erc721/src/lib.rs @@ -9,7 +9,7 @@ use openzeppelin_stylus::{ extensions::{Erc721Enumerable as Enumerable, IErc721Burnable}, Erc721, IErc721, }, - utils::Pausable, + utils::{introspection::erc165::IErc165, Pausable}, }; use stylus_sdk::{ abi::Bytes, @@ -152,11 +152,8 @@ impl Erc721Example { Ok(()) } - fn supports_interface( - interface_id: FixedBytes<4>, - ) -> Result> { - let interface_id = u32::from_be_bytes(*interface_id); - let supported = interface_id == ::INTERFACE_ID; - Ok(supported) + 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/erc721.rs b/examples/erc721/tests/erc721.rs index 357de749..cfc23d3d 100644 --- a/examples/erc721/tests/erc721.rs +++ b/examples/erc721/tests/erc721.rs @@ -2109,10 +2109,17 @@ async fn support_interface(alice: Account) -> eyre::Result<()> { assert_eq!(supports_interface, false); - let valid_interface_id: u32 = 0x_80ac58cd; + let erc721_interface_id: u32 = 0x80ac58cd; let Erc721::supportsInterfaceReturn { supportsInterface: supports_interface, - } = contract.supportsInterface(valid_interface_id.into()).call().await?; + } = 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);