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 15 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
122 changes: 122 additions & 0 deletions contracts-proc/src/interface_id.rs
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" {
Copy link
Member Author

@qalisander qalisander Sep 19, 2024

Choose a reason for hiding this comment

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

Here we retrieve all attributes of type: #[selector(name = "actualSelectorName") in case actual selector's name should be different from rust function's name. Erc721 is best example of it.

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
64 changes: 64 additions & 0 deletions contracts-proc/src/lib.rs
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)
}
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
24 changes: 24 additions & 0 deletions contracts/src/token/erc20/extensions/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.
///
Expand Down Expand Up @@ -76,3 +81,22 @@ impl IErc20Metadata for Erc20Metadata {
DEFAULT_DECIMALS
}
}

impl IErc165 for Erc20Metadata {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
}
}
28 changes: 27 additions & 1 deletion contracts/src/token/erc20/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down Expand Up @@ -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>>;
Expand Down Expand Up @@ -286,6 +290,13 @@ impl IErc20 for Erc20 {
}
}

impl IErc165 for Erc20 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not sure if we want to have it implemented always.

Copy link
Member Author

@qalisander qalisander Sep 21, 2024

Choose a reason for hiding this comment

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

Actually OZ Erc20 doesn't have Erc165 support, compare to Erc721. I think the original issue is here was closed due to "complexity". Some other implementations of Erc20 standard (not from open zeppelin) have it.
Also seeking around github by actual Erc20 interface id 0x36372b07, I could find projects that rely on the Erc20 with interface support.
In fact at this pr we are not forcing users to expose supportsInterface(bytes4). But they have an option to do so.
@ggonzalez94 what do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
}
Loading
Loading