Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: erc165 support interface #281

Merged
merged 21 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"contracts",
"contracts-proc",
"lib/crypto",
"lib/motsu",
"lib/motsu-proc",
Expand All @@ -21,6 +22,7 @@ members = [
]
default-members = [
"contracts",
"contracts-proc",
"lib/crypto",
"lib/motsu",
"lib/motsu-proc",
Expand Down
22 changes: 22 additions & 0 deletions contracts-proc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "openzeppelin-stylus-proc"
description = "Procedural macros for OpenZeppelin Stylus contracts"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description = "Procedural macros for OpenZeppelin Stylus contracts"
description = "Procedural macros for OpenZeppelin Stylus smart contracts library."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should also change description of contracts from OpenZeppelin Contracts for Stylus to OpenZeppelin smart contracts library for Stylus? Just seeking for consistency among projects.
cc @ggonzalez94

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the original one slightly more, since it is more concise. But I don't have a strong opinion tbh. Agreed with your comment @qalisander that we should keep both consistent.

version = "0.1.0"
qalisander marked this conversation as resolved.
Show resolved Hide resolved
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
118 changes: 118 additions & 0 deletions contracts-proc/src/interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
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 interface id as an associated constant for the trait.
pub(crate) fn interface(_attr: TokenStream, input: TokenStream) -> TokenStream {

Check warning on line 13 in contracts-proc/src/interface.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] contracts-proc/src/interface.rs#L13

warning: this argument is passed by value, but not consumed in the function body --> contracts-proc/src/interface.rs:13:32 | 13 | pub(crate) fn interface(_attr: TokenStream, input: TokenStream) -> TokenStream { | ^^^^^^^^^^^ help: consider taking a reference instead: `&TokenStream` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value = note: `-W clippy::needless-pass-by-value` implied by `-W clippy::pedantic` = help: to override `-W clippy::pedantic` add `#[allow(clippy::needless_pass_by_value)]`
Raw output
contracts-proc/src/interface.rs:13:32:w:warning: this argument is passed by value, but not consumed in the function body
  --> contracts-proc/src/interface.rs:13:32
   |
13 | pub(crate) fn interface(_attr: TokenStream, input: TokenStream) -> TokenStream {
   |                                ^^^^^^^^^^^ help: consider taking a reference instead: `&TokenStream`
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value
   = note: `-W clippy::needless-pass-by-value` implied by `-W clippy::pedantic`
   = help: to override `-W clippy::pedantic` add `#[allow(clippy::needless_pass_by_value)]`


__END__

Check warning on line 13 in contracts-proc/src/interface.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] contracts-proc/src/interface.rs#L13

warning: this argument is passed by value, but not consumed in the function body --> contracts-proc/src/interface.rs:13:32 | 13 | pub(crate) fn interface(_attr: TokenStream, input: TokenStream) -> TokenStream { | ^^^^^^^^^^^ help: consider taking a reference instead: `&TokenStream` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value = note: `-W clippy::needless-pass-by-value` implied by `-W clippy::pedantic` = help: to override `-W clippy::pedantic` add `#[allow(clippy::needless_pass_by_value)]`
Raw output
contracts-proc/src/interface.rs:13:32:w:warning: this argument is passed by value, but not consumed in the function body
  --> contracts-proc/src/interface.rs:13:32
   |
13 | pub(crate) fn interface(_attr: TokenStream, input: TokenStream) -> TokenStream {
   |                                ^^^^^^^^^^^ help: consider taking a reference instead: `&TokenStream`
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value
   = note: `-W clippy::needless-pass-by-value` implied by `-W clippy::pedantic`
   = help: to override `-W clippy::pedantic` add `#[allow(clippy::needless_pass_by_value)]`


__END__
ggonzalez94 marked this conversation as resolved.
Show resolved Hide resolved
let mut input = parse_macro_input!(input as ItemTrait);
let mut output = quote! {};

let mut selectors = Vec::new();
for item in &mut input.items {
let TraitItem::Fn(func) = item else {
continue;
};

let mut override_name = None;
for attr in mem::take(&mut func.attrs) {
let Some(ident) = attr.path().get_ident() else {
func.attrs.push(attr);
continue;
};
if *ident == "selector" {
if override_name.is_some() {
error!(attr.path(), "more than one selector attribute");
}
let args: SelectorArgs = match attr.parse_args() {
Ok(args) => args,
Err(error) => error!(ident, "{}", error),
};
override_name = Some(args.name);
continue;
}
func.attrs.push(attr);
}

let sol_name = override_name.unwrap_or_else(|| {
func.sig.ident.clone().to_string().to_case(Case::Camel)
});

let args = func.sig.inputs.iter();
let arg_types: Vec<_> = args
.filter_map(|arg| match arg {
FnArg::Typed(t) => Some(t.ty.clone()),
_ => None,

Check warning on line 51 in contracts-proc/src/interface.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] contracts-proc/src/interface.rs#L51

warning: wildcard matches only a single variant and will also match any future added variants --> contracts-proc/src/interface.rs:51:17 | 51 | _ => None, | ^ help: try: `FnArg::Receiver(_)` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#match_wildcard_for_single_variants = note: `-W clippy::match-wildcard-for-single-variants` implied by `-W clippy::pedantic` = help: to override `-W clippy::pedantic` add `#[allow(clippy::match_wildcard_for_single_variants)]`
Raw output
contracts-proc/src/interface.rs:51:17:w:warning: wildcard matches only a single variant and will also match any future added variants
  --> contracts-proc/src/interface.rs:51:17
   |
51 |                 _ => None,
   |                 ^ help: try: `FnArg::Receiver(_)`
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#match_wildcard_for_single_variants
   = note: `-W clippy::match-wildcard-for-single-variants` implied by `-W clippy::pedantic`
   = help: to override `-W clippy::pedantic` add `#[allow(clippy::match_wildcard_for_single_variants)]`


__END__

Check warning on line 51 in contracts-proc/src/interface.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] contracts-proc/src/interface.rs#L51

warning: wildcard matches only a single variant and will also match any future added variants --> contracts-proc/src/interface.rs:51:17 | 51 | _ => None, | ^ help: try: `FnArg::Receiver(_)` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#match_wildcard_for_single_variants = note: `-W clippy::match-wildcard-for-single-variants` implied by `-W clippy::pedantic` = help: to override `-W clippy::pedantic` add `#[allow(clippy::match_wildcard_for_single_variants)]`
Raw output
contracts-proc/src/interface.rs:51:17:w:warning: wildcard matches only a single variant and will also match any future added variants
  --> contracts-proc/src/interface.rs:51:17
   |
51 |                 _ => None,
   |                 ^ help: try: `FnArg::Receiver(_)`
   |
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#match_wildcard_for_single_variants
   = note: `-W clippy::match-wildcard-for-single-variants` implied by `-W clippy::pedantic`
   = help: to override `-W clippy::pedantic` add `#[allow(clippy::match_wildcard_for_single_variants)]`


__END__
})
.collect();

let selector = quote! { u32::from_be_bytes(stylus_sdk::function_selector!(#sol_name #(, #arg_types )*)) };
selectors.push(selector);
}

let name = input.ident.clone();
let vis = input.vis.clone();
let attrs = input.attrs.clone();
let trait_items = input.items.clone();
let (_impl_generics, ty_generics, where_clause) =
input.generics.split_for_impl();

output.extend(quote! {
#(#attrs)*
#vis trait #name #ty_generics #where_clause {
#(#trait_items)*

/// Solidity interface id associated with current trait.
const INTERFACE_ID: u32 = {
#(#selectors)^*
};
}
});

output.into()
}

struct SelectorArgs {
name: String,
}

impl Parse for SelectorArgs {
fn parse(input: ParseStream) -> Result<Self> {
let mut name = None;

if input.is_empty() {
error!(@input.span(), "missing id or text argument");
}

while !input.is_empty() {
let ident: Ident = input.parse()?;
let _: Token![=] = input.parse()?;

match ident.to_string().as_str() {
"name" => {
let lit: LitStr = input.parse()?;
if name.is_some() {
error!(@lit, r#"only one "name" is allowed"#);
}
name = Some(lit.value());
}
_ => error!(@ident, "Unknown selector attribute"),
}

// allow a comma
let _: Result<Token![,]> = input.parse();
}

if let Some(name) = name {
Ok(Self { name })
} else {
error!(@input.span(), r#""name" is required"#);
}
}
}
23 changes: 23 additions & 0 deletions contracts-proc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// TODO#q: add crate documentation.

extern crate proc_macro;

Check warning on line 3 in contracts-proc/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] contracts-proc/src/lib.rs#L3

warning: missing documentation for the crate --> contracts-proc/src/lib.rs:3:1 | 3 | / extern crate proc_macro; 4 | | use proc_macro::TokenStream; 5 | | 6 | | /// Shorthand to print nice errors. ... | 22 | | interface::interface(attr, input) 23 | | } | |_^ | = note: requested on the command line with `-W missing-docs`
Raw output
contracts-proc/src/lib.rs:3:1:w:warning: missing documentation for the crate
  --> contracts-proc/src/lib.rs:3:1
   |
3  | / extern crate proc_macro;
4  | | use proc_macro::TokenStream;
5  | |
6  | | /// Shorthand to print nice errors.
...  |
22 | |     interface::interface(attr, input)
23 | | }
   | |_^
   |
   = note: requested on the command line with `-W missing-docs`


__END__

Check warning on line 3 in contracts-proc/src/lib.rs

View workflow job for this annotation

GitHub Actions / clippy

[clippy] contracts-proc/src/lib.rs#L3

warning: missing documentation for the crate --> contracts-proc/src/lib.rs:3:1 | 3 | / extern crate proc_macro; 4 | | use proc_macro::TokenStream; 5 | | 6 | | /// Shorthand to print nice errors. ... | 22 | | interface::interface(attr, input) 23 | | } | |_^ | = note: requested on the command line with `-W missing-docs`
Raw output
contracts-proc/src/lib.rs:3:1:w:warning: missing documentation for the crate
  --> contracts-proc/src/lib.rs:3:1
   |
3  | / extern crate proc_macro;
4  | | use proc_macro::TokenStream;
5  | |
6  | | /// Shorthand to print nice errors.
...  |
22 | |     interface::interface(attr, input)
23 | | }
   | |_^
   |
   = note: requested on the command line with `-W missing-docs`


__END__
use proc_macro::TokenStream;

/// Shorthand to print nice errors.
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;

/// Computes interface id as an associated constant for the trait.
#[proc_macro_attribute]
pub fn interface(attr: TokenStream, input: TokenStream) -> TokenStream {
interface::interface(attr, input)
}
1 change: 1 addition & 0 deletions contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ stylus-sdk.workspace = true
stylus-proc.workspace = true
mini-alloc.workspace = true
keccak-const.workspace = true
openzeppelin-stylus-proc = { path = "../contracts-proc"}
qalisander marked this conversation as resolved.
Show resolved Hide resolved

[dev-dependencies]
alloy-primitives = { workspace = true, features = ["arbitrary"] }
Expand Down
10 changes: 10 additions & 0 deletions contracts/src/token/erc20/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//! [`Erc20`] applications.
use alloy_primitives::{Address, U256};
use alloy_sol_types::sol;
use openzeppelin_stylus_proc::interface;
use stylus_proc::SolidityError;
use stylus_sdk::{
call::MethodError,
Expand Down Expand Up @@ -111,6 +112,7 @@ sol_storage! {
}

/// Required interface of an [`Erc20`] compliant contract.
#[interface]
pub trait IErc20 {
/// The error type associated to this ERC-20 trait implementation.
type Error: Into<alloc::vec::Vec<u8>>;
Expand Down Expand Up @@ -550,6 +552,7 @@ mod tests {
use stylus_sdk::msg;

use super::{Erc20, Error, IErc20};
use crate::token::erc721::{Erc721, IErc721};

#[motsu::test]
fn reads_balance(contract: Erc20) {
Expand Down Expand Up @@ -882,4 +885,11 @@ 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 = 0x_36372b07;
assert_eq!(actual, expected);
}
}
10 changes: 10 additions & 0 deletions contracts/src/token/erc721/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use alloc::vec;

use alloy_primitives::{fixed_bytes, uint, Address, FixedBytes, U128, U256};
use openzeppelin_stylus_proc::interface;
use stylus_sdk::{
abi::Bytes,
alloy_sol_types::sol,
Expand Down Expand Up @@ -198,6 +199,7 @@ sol_storage! {
unsafe impl TopLevelStorage for Erc721 {}

/// Required interface of an [`Erc721`] compliant contract.
#[interface]
pub trait IErc721 {
/// The error type associated to this ERC-721 trait implementation.
type Error: Into<alloc::vec::Vec<u8>>;
Expand Down Expand Up @@ -317,6 +319,7 @@ pub trait IErc721 {
/// # Events
///
/// Emits a [`Transfer`] event.
#[selector(name = "safeTransferFrom")]
fn safe_transfer_from_with_data(
&mut self,
from: Address,
Expand Down Expand Up @@ -2487,4 +2490,11 @@ mod tests {
}) if token_id == t_id
));
}

#[motsu::test]
fn interface_id() {
let actual = <Erc721 as IErc721>::INTERFACE_ID;
let expected = 0x_80ac58cd;
assert_eq!(actual, expected);
}
}
10 changes: 9 additions & 1 deletion examples/erc20/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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},
Expand Down Expand Up @@ -105,4 +105,12 @@ 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>,
) -> Result<bool, Vec<u8>> {
let interface_id = u32::from_be_bytes(*interface_id);
let supported = interface_id == <Erc20 as IErc20>::INTERFACE_ID;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also should check for IErc165 interface id

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be done relatively straight forward with something like this(not sure if FixedBytes<4> can be converted to [u8; 4] and the return type can be simplified, but I did it for the simplicity of the example):

pub trait ERC165 {
    const INTERFACE_ID: [u8; 4] = [0x01, 0xff, 0xff, 0xff];

    fn supports_erc165_interface(&self, interface_id: [u8; 4]) -> bool {
        interface_id == Self::INTERFACE_ID
    }
}

And then this contract can do:

impl ERC165 for Erc20Example {}
impl Erc20Example {
    fn supports_interface(&self, interface_id: [u8; 4]) -> bool {
        interface_id == <Erc20 as IErc20>::INTERFACE_ID ||
        ERC165::supports_erc165_interface(self, interface_id)
    }
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure there are better ways to achieve the same, but something like this should work

Ok(supported)
}
}
2 changes: 2 additions & 0 deletions examples/erc20/tests/abi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
26 changes: 26 additions & 0 deletions examples/erc20/tests/erc20.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1340,3 +1340,29 @@ async fn error_when_transfer_from(alice: Account, bob: Account) -> Result<()> {

Ok(())
}

#[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 = 0x_ffffffff;
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::supportsInterfaceReturn {
supportsInterface: supports_interface,
} = contract.supportsInterface(valid_interface_id.into()).call().await?;

assert_eq!(supports_interface, true);

Ok(())
}
10 changes: 9 additions & 1 deletion examples/erc721/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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},
Expand Down Expand Up @@ -151,4 +151,12 @@ impl Erc721Example {

Ok(())
}

fn supports_interface(
interface_id: FixedBytes<4>,
) -> Result<bool, Vec<u8>> {
let interface_id = u32::from_be_bytes(*interface_id);
let supported = interface_id == <Erc721 as IErc721>::INTERFACE_ID;
Ok(supported)
}
}
5 changes: 5 additions & 0 deletions examples/erc721/tests/abi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,25 @@ 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 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);
Expand Down
Loading
Loading