-
Notifications
You must be signed in to change notification settings - Fork 17
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
Changes from 15 commits
9a3200a
5811bf2
94ed516
252264f
de539d3
0b433b7
402e05e
0615b0f
9229b32
bec9e4c
e385668
4b6eea2
4e0f887
80af3b0
7be160e
7e6a88e
c83e84b
96989da
4cf685e
eb48b39
7d27027
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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" | ||
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
//! 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_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" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here we retrieve all attributes of type: |
||
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); | ||
} | ||
qalisander marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
let sol_name = override_name | ||
qalisander marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.unwrap_or_else(|| func.sig.ident.to_string().to_case(Case::Camel)); | ||
|
||
let arg_types: Vec<_> = func | ||
.sig | ||
.inputs | ||
.iter() | ||
.filter_map(|arg| match arg { | ||
FnArg::Typed(t) => Some(t.ty.clone()), | ||
FnArg::Receiver(_) => None, | ||
}) | ||
.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(); | ||
|
||
quote! { | ||
#(#attrs)* | ||
#vis trait #name #ty_generics #where_clause { | ||
#(#trait_items)* | ||
|
||
/// Solidity interface id associated with current trait. | ||
const INTERFACE_ID: u32 = { | ||
#(#selectors)^* | ||
qalisander marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
} | ||
} | ||
.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"#); | ||
} | ||
} | ||
} | ||
qalisander marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 interface id as an associated constant `INTERFACE_ID` for the trait | ||
qalisander marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// 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) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure if we want to have it implemented always. |
||
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); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,15 +4,18 @@ | |
//! 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, | ||
evm, msg, | ||
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<alloc::vec::Vec<u8>>; | ||
|
@@ -286,6 +290,13 @@ impl IErc20 for Erc20 { | |
} | ||
} | ||
|
||
impl IErc165 for Erc20 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure if we want to have it implemented always. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually OZ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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. | ||
|
@@ -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 = <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); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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
toOpenZeppelin smart contracts library for Stylus
? Just seeking for consistency among projects.cc @ggonzalez94
There was a problem hiding this comment.
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.