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

feat: erc165 support interface #281

merged 21 commits into from
Sep 27, 2024

Conversation

qalisander
Copy link
Member

@qalisander qalisander commented Sep 11, 2024

Adds erc165 standard altogether with a proc macro, that lets to compute interface id in a simple way.

Resolves #33

PR Checklist

  • Tests
  • Documentation

Copy link

netlify bot commented Sep 11, 2024

Deploy Preview for contracts-stylus canceled.

Name Link
🔨 Latest commit 7d27027
🔍 Latest deploy log https://app.netlify.com/sites/contracts-stylus/deploys/66f1a96284edb00008522be9

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

Copy link

codecov bot commented Sep 11, 2024

Copy link
Collaborator

@ggonzalez94 ggonzalez94 left a comment

Choose a reason for hiding this comment

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

This is looking good! I like the approach of making this a procedural macro. I still need to go through the macro implementation, but already left a couple comments

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
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)
    }
}

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

contracts-proc/src/interface.rs Outdated Show resolved Hide resolved
Copy link
Collaborator

@bidzyyys bidzyyys left a comment

Choose a reason for hiding this comment

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

Great job @qalisander!
I am not a fan of introducing contracts-proc lib only for a single macro.
I am thinking if is there any way to achieve the same only with default trait impls like:

pub trait IInterface {
    const INTERFACE_ID: FixedBytes<4>;
}
pub trait IErc165: IInterface { 
    const ERC165_INTERFACE_ID: FixedBytes<4> = fixed_bytes!("01ffffff"); 
    
    fn supports_interface(&self, interface_id: FixedBytes<4>) -> bool { 
        interface_id == Self::ERC165_INTERFACE_ID || interface_id == Self::INTERFACE_ID
    } 
}

Could you try this out? Or if you see any blockers let me know before spending time on it.

contracts-proc/src/interface.rs Outdated Show resolved Hide resolved
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.

@qalisander qalisander marked this pull request as ready for review September 19, 2024 20:22
@qalisander
Copy link
Member Author

qalisander commented Sep 20, 2024

One additional advantage of having procedural macro for interface ids, instead of hard-coding them is: Whenever we will accidentally change/refactor contract's trait or stylus-sdk will introduce update that will slightly change selector or interface id computation. It will be caught by our unit and integration tests.

Copy link
Collaborator

@bidzyyys bidzyyys left a comment

Choose a reason for hiding this comment

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

Looks good overall, left comments that bothers me.

contracts-proc/Cargo.toml Outdated Show resolved Hide resolved
contracts/Cargo.toml Outdated Show resolved Hide resolved
@@ -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.

examples/erc20/src/lib.rs Show resolved Hide resolved

pub fn supports_interface(interface_id: FixedBytes<4>) -> bool {
Erc721::supports_interface(interface_id)
|| Enumerable::supports_interface(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.

Why Enumerable?

Copy link
Member Author

Choose a reason for hiding this comment

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

Basically it is implemented for Erc721Enumerable in solidity

mod tests {
// use crate::token::erc721::extensions::{Erc721Metadata, IErc721Metadata};

// TODO: IErc721Metadata should be refactored to have same api as solidity
Copy link
Collaborator

Choose a reason for hiding this comment

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

TODO

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.

You implemented metadata extension different from solidity. It has base_uri function instead of token_uri. And now it exposes baseUri() function into abi that is not like that in solidity. If you have ideas how to refactor it right now we can do. Otherwise make sense to do it later and create an issue for that
cc @ggonzalez94

@@ -549,6 +555,13 @@ impl IErc721 for Erc721 {
}
}

impl IErc165 for Erc721 {
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

Choose a reason for hiding this comment

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

It is implemented in solidity for Erc721

Copy link
Collaborator

Choose a reason for hiding this comment

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

We will have to decide on an approach for all the standards in our library, so we can use this thread to do so(I've also opened an internal chat to get our Solidity team opinion). I'm leaning more on not adding it by default and letting the user opt in when they want.
That being said when the standard defines that ERC165 should be part of the contract we should do it. That's the case with ERC721

Copy link
Collaborator

Choose a reason for hiding this comment

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

@qalisander after discussing internally and doing some research it seems ERC165 is not as widely used that is worth adding it to all our contract. What I suggest we do:

  1. Add it automatically to only the standards that require it as part of the ERC(e.g ERC721)
  2. Add a section in our docs that explains how to add it to your own contract. This could be under the API reference for ERC165 or similar.

What do you think?

Copy link
Member Author

@qalisander qalisander Sep 25, 2024

Choose a reason for hiding this comment

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

I think that would look a bit inconsistent. I even wondering do we need ERC165 that much just for ERC721 .. Also I suspect, it is not widely used mostly, because it is not implemented for the contracts like erc20.

  1. Consider that rust is different from solidity. And with current implementation just to make support_interface function visible, library user should import openzeppelin_stylus::utils::introspection::erc165::IErc165 module in the scope (otherwise it won't be even compiled). And then reexport it within #[public] macro. So we are not "automatically" implementing the standard as I see, but making it simple to implement for users. At least what do you mean by "automatically" practically?
  2. That's a great point. I need to add a guide about erc165 to antora docs. But seems for me the ERC165 guide (for erc20) would be more simple if we would implement IErc165 trait for ERC20 also.

Copy link
Member Author

@qalisander qalisander Sep 25, 2024

Choose a reason for hiding this comment

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

And one more point. There is an "orphan rule" in rust that disallows to user implement foreign trait (like IErc165 in our case) for a foreign contract type (like Erc20). In this case the IErc165 trait seems even less practical for users if they would try to implement it for Erc20. May be it would even make sense to remove it.

Copy link
Collaborator

@ggonzalez94 ggonzalez94 Sep 25, 2024

Choose a reason for hiding this comment

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

I think that would look a bit inconsistent.

I don't think it is inconsistent if we explicitly say: We will only add it to standards that require it and give the user the option to add it to any contract(s) they think is worth it.

I even wondering do we need ERC165 that much just for ERC721

It wouldn't be for ERC721 only, since the implementation would be available for anyone wanting to add this to contracts they would like to specify which interfaces they support.

And then reexport it within #[public] macro. So we are not "automatically" implementing the standard as I see, but making it simple to implement for users.

What do you mean here? We are adding it to the #[public] impl block of contracts. That means it would automatically be part of the inherited contract api right?

Copy link
Member Author

@qalisander qalisander Sep 25, 2024

Choose a reason for hiding this comment

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

What do you mean here? We are adding it to the #[public] impl block of contracts. That means it would automatically be part of the inherited contract api right?

We are doing so inside examples. User also can opt out to export it to the abi, even if trait IErc165 is implemented in our library. That is the difference from solidity, where as I see Erc721 will always have supportInterface(...) abi public

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok... after discussing this today I agree it makes sense to add it to implementations of main interfaces and it doesn't come at any cost to users that don't want it(since it is not part of the public abi and they still need to explicitely opt in). Only potential downside is a bit of gas overhead when inheriting from multiple contracts.

contracts/src/utils/introspection/erc165.rs Outdated Show resolved Hide resolved
@@ -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.

@@ -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.

Copy link
Collaborator

@ggonzalez94 ggonzalez94 left a comment

Choose a reason for hiding this comment

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

Great job here! I just did a second pass and is looking good.
I left a few comments/suggestions and tomorrow I'll go through the open questions

@@ -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.

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.

contracts-proc/src/interface_id.rs Outdated Show resolved Hide resolved
contracts-proc/src/interface_id.rs Outdated Show resolved Hide resolved
contracts-proc/src/interface_id.rs Outdated Show resolved Hide resolved
contracts-proc/src/lib.rs Outdated Show resolved Hide resolved
contracts-proc/src/interface_id.rs Show resolved Hide resolved
Copy link
Collaborator

@ggonzalez94 ggonzalez94 left a comment

Choose a reason for hiding this comment

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

@qalisander this looks ready to go IMO. The only thing left to do is documentation, but you can decide if you want to include it as part of this PR or create a new one for that

@qalisander qalisander merged commit 9b04143 into main Sep 27, 2024
22 checks passed
qalisander added a commit that referenced this pull request Oct 4, 2024
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>
(cherry picked from commit 9b04143)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support interface ERC165 to our contracts
3 participants