Skip to content

Commit

Permalink
feat: erc165 support interface (#281)
Browse files Browse the repository at this point in the history
Adds erc165 standard altogether with a proc macro, that lets to compute
interface id in a simple way.

<!-- Fill in with issue number -->
Resolves #33

#### PR Checklist

- [x] Tests
- [x] Documentation

---------

Co-authored-by: Daniel Bigos <daniel.bigos@icloud.com>
  • Loading branch information
qalisander and bidzyyys authored Sep 27, 2024
1 parent 61000df commit 9b04143
Show file tree
Hide file tree
Showing 21 changed files with 506 additions and 9 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/check-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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.

3 changes: 3 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 Expand Up @@ -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" }
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"
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
102 changes: 102 additions & 0 deletions contracts-proc/src/interface_id.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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'")
}
}
}
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 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<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.workspace = true

[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 {
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 {
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

0 comments on commit 9b04143

Please sign in to comment.