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

Support ERC-165 identifiers #7856

Closed
fulldecent opened this issue Dec 2, 2019 · 31 comments · Fixed by #8642
Closed

Support ERC-165 identifiers #7856

fulldecent opened this issue Dec 2, 2019 · 31 comments · Fixed by #8642
Assignees
Milestone

Comments

@fulldecent
Copy link
Contributor

fulldecent commented Dec 2, 2019

Implement type(I).interfaceId as the XOR of the function selectors of the external interface of an interface or contract excluding inherited functions.

The tests should include a full ERC-165 implementation that includes inheritance.


Original text:

We can add ERC-165 support to Solidity like this:

Test case

interface I {
}

contract C {
    private bytes4 a = I.erc165interfaceid;
}

Negative test cases

interface I {
}

library L {
}

contract C {
    private bytes4 a = C.erc165interfaceid; // ❌ ERROR: contracts do not have ERC-165 interface identifiers
    private bytes4 b = L.erc165interfaceid; // ❌ ERROR: libraries do not have ERC-165 interface identifiers

    function f() returns () {
        private bytes4 c = this.erc165interfaceid; // ❌ ERROR: instances do not have ERC-165 interface identifiers    
    }
}

Useful example

interface ERCX is ERC165
{
    // .
}

interface ERC165
{
  /**
   * @dev Checks if the smart contract includes a specific interface.
   * @notice This function uses less than 30,000 gas.
   * @param _interfaceID The interface identifier, as specified in ERC-165.
   * @return True if _interfaceID is supported, false otherwise.
   */
  function supportsInterface(bytes4 _interfaceID)  external view returns (bool);    
}

contract MyContract is ERC165, ERCX
{
  mapping(bytes4 => bool) internal supportedInterfaces;

  constructor() public 
  {
    supportedInterfaces[ERC165.erc165interfaceid] = true;
    supportedInterfaces[ERCX.erc165interfaceid] = true;
  }

  function supportsInterface(bytes4 _interfaceID) external view returns (bool)
  {
    return supportedInterfaces[_interfaceID];
  }
}

Discussion

I just want to note that the convention (established by ENS and followed since then) is:

the ERC-165 identifier for an interface B, which extends an interface A, shall be based only on the members of B.

Test case

pragma solidity ^0.5;

interface I { function a() external; }
interface I2 is I { function b() external; }

contract C {
    constructor() public {
        I2 i;
        assert(I2.erc165interfaceid == i.b.selector);
    }
}

References

https://eips.ethereum.org/EIPS/eip-165

@chriseth
Copy link
Contributor

chriseth commented Dec 3, 2019

I'm a bit reluctant to add explicit ERC numbers to the Solidity language, but I think this is a good feature to have. We should consider adding it to type(I), though.

@axic
Copy link
Member

axic commented Dec 12, 2019

A lot of discussion took place in #1447.

@chriseth
Copy link
Contributor

This could be non-breaking.

@chriseth
Copy link
Contributor

Discussion: it should be a member of type(I).

Brainstorming about names:

  • ercID(165)
  • interface
  • interfaceid
  • interfaceID
  • mask

@christianparpart
Copy link
Member

So, also as a sum-up for myself:

After some discussion IRL, I'd like to propose the following:

  • Since type(arg) already exists, a compile-time evaluated checksum member can be added to return the checksum of arg.
  • type(FunctionSignature).checksum returns the checksum of a function signature, i.e. bytes4(keccak256(FunctionSignature)), which happens to be equivalent to the function signature
  • type(Interface_or_Contract).checksum returns the checksum of each public function xor'd toegher.
  • type(Contract as Interface).checksum same as above, except that only public member functions from Interface in Contract are taken into account. -- The word as does not need to be a new keyword, as it is a context dependent word, and be treated like a directive.

The last case would be to construct the checksum of a type (contract) to be able to match it for conformance against, for example, ERC721.

@axic
Copy link
Member

axic commented Dec 16, 2019

@christianparpart see my proposal in #1447 (comment), it just needs the ability to pass around interface types.

@chriseth
Copy link
Contributor

About inheritance: We could have two members, one that only concerns the functions in the interface itself and another one that also includes inherited members. They can be combined by xor, so people can use them how they like.

@fulldecent
Copy link
Contributor Author

Brainstorming:

  • ERC721.bytes4Id
  • ERC721.bytes4IdWithAncestors

Leaving open the possibility that ERC-165 will be superseded.

@chriseth
Copy link
Contributor

chriseth commented Feb 10, 2020

type(I).inheritedInterfaceID - type(I).intrinsicInterfaceID

@chriseth chriseth added this to the Sprint 2 milestone Feb 10, 2020
@chriseth
Copy link
Contributor

@fulldecent @jbaylina @nventuro @frangio We kindly need your input here. We would like to add eip 165 support as a feature to Solidity. Unfortunately, I was not able to find out how EIP 165 is defined with regards to inheritance. If a contract C inherits from A and B, what is its interface identifier? Is it the xor of the function selectors defined in A, B and C or just the ones added by C?

@frangio
Copy link
Contributor

frangio commented Feb 10, 2020

A contract can support more than a one interface identifier, so it's not possible to say which should be the identifier for a contract. If C inherits from A and B, it may be that both A's and B's identifiers are relevant for the specific protocol, or C's, or any other combination.

The proposal at the beginning of this issue suggested to only support asking for the identifier of an interface. This is interesting because interfaces don't support inheritance, so it wouldn't be a problem in that case.

@chriseth
Copy link
Contributor

chriseth commented Feb 11, 2020

@frangio "unfortunately", interfaces now do support inheritance. My intuitive approach would be to include all functions, inherited or not. If you want to compare against multiple interfaces, you would of course have to use something like

interface A { ... }
interface B { ... }
interface C is A, B { ... }
contract Contr is C {
    function supportsInterface(bytes4 _interfaceID)  external view returns (bool) {
      return
        _interfaceID == type(A).interfaceID ||
        _interfaceID == type(B).interfaceID ||
        _interfaceID == type(C).interfaceID;
    }
}

@chriseth chriseth removed this from the Sprint 2 milestone Feb 11, 2020
@frangio
Copy link
Contributor

frangio commented Feb 11, 2020

I think I share that intuition. However, in the spec for ERC721, the optional interfaces were documented as:

interface ERC721Metadata /* is ERC721 */ {

The inheritance was commented out because at the moment it wasn't allowed, but it would nowadays be uncommented. What would you say in this case? What's needed is the id for the uninherited functions of ERC721Metadata. Is the snippet wrong in using inheritance or would Solidity be wrong in producing the id for the full list of functions?

@chriseth
Copy link
Contributor

chriseth commented Feb 12, 2020

@frangio you are right, this is a place where we would need the interface id of the "intrinsic" interface. Other solutions:

  • defining an interface without erc165 and inheriting from that as well as 165.
  • using type(ERC721Metadata).inerfaceID ^ type(ERC164).interfaceID

@fulldecent
Copy link
Contributor Author

The ERC-165 identifier uses only the functions directly in the interface and it excludes the functions from the inherited interfaces.

Test case 1:

interface B {
    function f();
}

interface I is B { // The ERC-165 signature uses only the function g
    function g();
}

Test case 2:

interface B {
    function f();
}

interface I is B { // The ERC-165 signature uses functions f and g
    function f();
    function g();
}

This is not explicitly stated in ERC-165 (sorry!). But it is implied with its reference to Ethereum Name Service. And ENS prescribes the above process with its dependent interfaces.

Other than the ERC-165 standard, the above specification is already implemented in ERC-721 and ERC-1155. For example, ERC-1155 states interface ERC1155 /* is ERC165 */ but the interface identifier is specified and it is calculated WITHOUT the function from ERC-165.


I believe Solidity should NOT support an ERC-165 interface calculation for contracts. Arguments:

  1. This would be non-standard, only "interfaces" are specified in ERC-165
  2. An ERC-165 interface identifier serves to promote interoperability, and if a contract wants to support interoperability then it should conform to an interface, thus making a ERC-165 contract identifier useless
  3. It is possible to add this feature later additively, if desired

@chriseth
Copy link
Contributor

@fulldecent thanks for the clarifications! Would you think that type(I).interfaceID is an appropriate name for the xor of the identifiers of the functions defined in I (excluding the inherited functions)?

@fulldecent
Copy link
Contributor Author

Yes, I think this is an appropriate name.

It is consistent with the function definition at https://eips.ethereum.org/EIPS/eip-165

@chriseth
Copy link
Contributor

@frangio @axic what is your opinion on disallowing this on contracts and only allowing it on actual interfaces?

@frangio
Copy link
Contributor

frangio commented Feb 27, 2020

I don't have a strong opinion but I feel that disallowing on contracts is kind of arbitrary. A contract defines an interface too.

@frangio
Copy link
Contributor

frangio commented Mar 10, 2020

@chriseth Has there been any decision regarding whether the interfaceId will include inherited functions? We need to decide whether to use inheritance in our interfaces for the upcoming OpenZeppelin Contracts 3.0. (OpenZeppelin/openzeppelin-contracts#2113)

@chriseth
Copy link
Contributor

@frangio no decision yet. We are leaning towards disallowing interfaceID for contracts at least for now. What is your take on whether it should include inherited functions or not? How is it used in the community?

@frangio
Copy link
Contributor

frangio commented Mar 11, 2020

I don't think there's sufficient precedent in the community yet. Most people probably still don't know that interfaces can inherit.

My intuition is that inherited functions should not be included for the id.

Edit: I realized that I'm contradicting my initial intuition that I shared a few comments back. I guess I'm still undecided on this. Sorry!

@chriseth
Copy link
Contributor

Yeah, I think most of us are :(

@nventuro
Copy link
Contributor

I also believe an interface's id should be computed exclusively based on the functions declared on that interface and not on inherited ones. And I take @fulldecent's comment about how interfaces are interpreted by ERC165 as confirmation that this is how we should proceed.

@chriseth
Copy link
Contributor

I talked to @jbaylina at EthCC and he mentioned he would intuitively think that interfaceID would include inherited functions, but he also said that he did not have much time to think about it.

How would an implementation of ERC165's supportsInterface work in both cases? Is it very different?

@nventuro
Copy link
Contributor

The issue with including inherited interfaces is that would only be useful when referring to that very specific contract, with its entire ABI. Often you're only concerned with a subset of it (e.g. does it support ERC165, does it support ERC20, etc), not the entire thing. I'm not sure I can think of scenarios where I'd use something else other than the 'intrinsic' interface id.

@chriseth
Copy link
Contributor

@nventuro I think the prime example here is an extension interface of ERC20. What should its interface ID be?

@nventuro
Copy link
Contributor

I think I see your point.

I was mostly thinking about contracts, not interfaces. I do believe though that 'intrinsic' would definitely be the way to go for contracts: if we're considering adding IDs for them, I'd rather they work the same way as interface IDs do.

@chriseth
Copy link
Contributor

It seems that most of the people who have a specific opinion think that inherited functions should not be included. Because of that, I would propose to just implement it like that now.

@elenadimitrova elenadimitrova added this to the Sprint 6 milestone Apr 8, 2020
@aarlt aarlt self-assigned this Apr 14, 2020
@lukehutch
Copy link
Contributor

@frangio "unfortunately", interfaces now do support inheritance. My intuitive approach would be to include all functions, inherited or not. If you want to compare against multiple interfaces, you would of course have to use something like

interface A { ... }
interface B { ... }
interface C is A, B { ... }
contract Contr is C {
    function supportsInterface(bytes4 _interfaceID)  external view returns (bool) {
      return
        _interfaceID == type(A).interfaceID ||
        _interfaceID == type(B).interfaceID ||
        _interfaceID == type(C).interfaceID;
    }
}

At present, given contract ERC1363 is ERC20, ERC165, the value of type(ERC1363).interfaceId is the XOR of only the function selectors of ERC1363 itself, not the function selectors of ERC20 or ERC165. It doesn't seem right that the interfaceId of contract ERC1363 is ERC20, ERC165 and contract ERC1363 /* is ERC20, ERC165 */ should be the same. Any comment, @chriseth?

Negative test cases

interface I {
}

library L {
}

contract C {
    private bytes4 a = C.erc165interfaceid; // ❌ ERROR: contracts do not have ERC-165 interface identifiers
    private bytes4 b = L.erc165interfaceid; // ❌ ERROR: libraries do not have ERC-165 interface identifiers

    function f() returns () {
        private bytes4 c = this.erc165interfaceid; // ❌ ERROR: instances do not have ERC-165 interface identifiers    
    }
}

Why does type(C).interfaceId not work for contracts C, when it works for interfaces?

TypeError: Member "interfaceId" not found or not visible after argument-dependent lookup in type(contract C).

I don't see why this syntax should work for interfaces but not contracts. If you want the ERC165 interfaceId for a contract, you currently have to manually extract the interface for the contract into another file. That just seems like unnecessary effort.

@gustavoboy106
Copy link

Brainstorming:

  • ERC721.bytes4Id
  • ERC721.bytes4IdWithAncestors

Leaving open the possibility that ERC-165 will be superseded.

Is the ERC superseded? What to use now?

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 a pull request may close this issue.