-
Notifications
You must be signed in to change notification settings - Fork 22
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
Conversation
✅ Deploy Preview for contracts-stylus canceled.
|
examples/erc20/src/lib.rs
Outdated
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; |
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.
It also should check for IErc165 interface id
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 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)
}
}
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'm sure there are better ways to achieve the same, but something like this should work
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.
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
examples/erc20/src/lib.rs
Outdated
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; |
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 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)
}
}
examples/erc20/src/lib.rs
Outdated
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; |
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'm sure there are better ways to achieve the same, but something like this should work
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.
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_id.rs
Outdated
func.attrs.push(attr); | ||
continue; | ||
}; | ||
if *ident == "selector" { |
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.
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.
5bfba36
to
80af3b0
Compare
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. |
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.
Looks good overall, left comments that bothers me.
@@ -0,0 +1,22 @@ | |||
[package] | |||
name = "openzeppelin-stylus-proc" | |||
description = "Procedural macros for OpenZeppelin Stylus contracts" |
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.
description = "Procedural macros for OpenZeppelin Stylus contracts" | |
description = "Procedural macros for OpenZeppelin Stylus smart contracts library." |
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
to OpenZeppelin 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.
|
||
pub fn supports_interface(interface_id: FixedBytes<4>) -> bool { | ||
Erc721::supports_interface(interface_id) | ||
|| Enumerable::supports_interface(interface_id) |
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.
Why Enumerable
?
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.
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 |
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.
TODO
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.
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 { |
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 am not sure if we want to have it implemented always.
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.
It is implemented in solidity for Erc721
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.
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
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.
@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:
- Add it automatically to only the standards that require it as part of the ERC(e.g ERC721)
- 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?
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 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.
- Consider that rust is different from solidity. And with current implementation just to make
support_interface
function visible, library user should importopenzeppelin_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? - 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 implementIErc165
trait forERC20
also.
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.
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.
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 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?
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.
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
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.
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.
@@ -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 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.
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.
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?
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.
@@ -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 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.
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.
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" |
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.
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.
@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
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 <[email protected]> (cherry picked from commit 9b04143)
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 <[email protected]> (cherry picked from commit 9b04143)
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 <[email protected]> (cherry picked from commit 9b04143)
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 <[email protected]> (cherry picked from commit 9b04143)
Adds erc165 standard altogether with a proc macro, that lets to compute interface id in a simple way.
Resolves #33
PR Checklist