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

ERC-165 Standard Interface Detection #881

Merged
merged 24 commits into from
Feb 21, 2018
Merged
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5e02514
A standard for interface detection, fixes #165
fulldecent Feb 13, 2018
7558488
Discuss optional interfaces per @dete advice
fulldecent Feb 13, 2018
224822c
Add two competing implementations
fulldecent Feb 14, 2018
4b7b916
Add author email addresses per latest EIP-X
fulldecent Feb 14, 2018
6e4394d
Update caching contract
fulldecent Feb 15, 2018
d39b388
Add author Konrad Feldmeier
fulldecent Feb 15, 2018
50f59da
Updated to use staticcall
fulldecent Feb 16, 2018
529ce1b
Update cache to use explicit return values of 32 byte length
fulldecent Feb 16, 2018
26c9118
"uses less than"
fulldecent Feb 16, 2018
ad70b8d
Function selectors are defined by Ethereum, not Solidity
fulldecent Feb 16, 2018
3928548
Remove authors @VoR0220 @Souptacular @GriffGreen
fulldecent Feb 16, 2018
b97350c
Copyediting review
fulldecent Feb 17, 2018
881f577
Bytes, version 0.4.20, thanks @veox
fulldecent Feb 19, 2018
734d68b
Use 32-byte words
fulldecent Feb 19, 2018
b675b4b
Update zero padding, thank you @veox
fulldecent Feb 20, 2018
12fe0b5
Switch from a cache to a query contract, cache is out of scope
fulldecent Feb 20, 2018
1947d2f
Complete Homer implementation
fulldecent Feb 20, 2018
490ce29
not a cache
fulldecent Feb 20, 2018
4ecf584
32 bytes input
fulldecent Feb 21, 2018
50ad36b
Hex, not string
fulldecent Feb 21, 2018
e902727
Nibbles are not bytes, duh
fulldecent Feb 21, 2018
d47923b
Update link
fulldecent Feb 21, 2018
03c67d2
interfaced -> interacted
fulldecent Feb 21, 2018
2cb6f91
Update eip-165.md
nicksavers Feb 21, 2018
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
247 changes: 247 additions & 0 deletions EIPS/eip-165.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
## Preamble

```
EIP: <to be assigned>
Title: ERC-165 Standard Interface Detection
Author: Christian Reitwießner <[email protected]>, Nick Johnson <[email protected]>, Fabian Vogelsteller <[email protected]>, Jordi Baylina <[email protected]>, Konrad Feldmeier <[email protected]>, William Entriken <[email protected]>
Type: Standard Track
Category: ERC
Status: Draft
Created: 2018-01-23
```

## Simple Summary

Creates a standard method to publish and detect what interfaces a smart contract implements.

## Abstract

Herein, we standardize the following:

1. How interfaces are identified
2. How a contract will publish the interfaces it implements
3. How to detect if a contract implements ERC-165
4. How to detect if a contract implements any given interface

## Motivation

For some "standard interfaces" like [the ERC-20 token interface](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.md), it is sometimes useful to query whether a contract supports the interface and if yes, which version of the interface, in order to adapt the way in which the contract is to be interfaced with. Specifically for ERC-20, a version identifier has already been proposed. This proposal stadardizes the concept of interfaces and standardizes the identification (naming) of interfaces.
Copy link
Contributor

Choose a reason for hiding this comment

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

Might as well link to https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md directly: the current link is a redirect.

Copy link
Contributor

@veox veox Feb 21, 2018

Choose a reason for hiding this comment

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

Style suggestion: "the way in which the contract is to be interfaced interacted with".


## Specification

### How Interfaces are Identified

For this standard, an *interface* is a set of [function selectors as defined by the Ethereum ABI](http://solidity.readthedocs.io/en/develop/abi-spec.html#function-selector). This a subset of [Solidity's concept of interfaces](http://solidity.readthedocs.io/en/develop/abi-spec.html) and the `interface` keyword definition which also defines return types, mutability and events.

We define the interface identifier as the XOR of all function selectors in the interface. This code example shows how to calculate an interface identifier:

```solidity
pragma solidity ^0.4.20;

interface Solidity101 {
function hello() external pure;
function world(int) external pure;
}

contract Selector {
function calculateSelector() public pure returns (bytes4) {
Solidity101 i;
return i.hello.selector ^ i.world.selector;
}
}
```

Note: interfaces do not permit optional functions, therefore, the interface identity will not include them.

### How a Contract will Publish the Interfaces it Implements

A contract that is compliant with ERC-165 shall implement the following interface (referred as `ERC165.sol`):

```solidity
pragma solidity ^0.4.20;

interface ERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
Copy link
Contributor

@chriseth chriseth Feb 16, 2018

Choose a reason for hiding this comment

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

I think it would be good to clarify this example. Does any ERC165-compliant interface need to specify supportsInterface as part of its interface? Also there should be other functions to have a real example. It might not be clear to the reader that there can be other functions and they should be part of the interface identifier.

It would probably also good to include supportsInteface in the Solidity101 contract.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hm, ok, I think I also just misunderstood this. supportsInterface can return true on multiple inputs and each input is the xor of a set of functions, right? Each ERC165-compliant contract must return true for the ERC165-interface id.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. Correct. For example, in ERC-721, you will return true for ERC-165, ERC-721, ERC-721-Metadata, ERC-721-Enumeration. These are all separate interfaces.

}
```

The interface identifier for this interface is `0x01ffc9a7`. You can calculate this by running ` bytes4(keccak256('supportsInterface(bytes4)'));` or using the `Selector` contract above.

Therefore the implementing contract will have a `supportsInterface` function that returns:

- `true` when `interfaceID` is `0x01ffc9a7` (EIP165 interface)
- `false` when `interfaceID` is `0xffffffff`
- `true` for any other `interfaceID` this contract implements
- `false` for any other `interfaceID`

This function must return a bool and use at most 30,000 gas.

Implementation note, there are several logical ways to implement this function. Please see the example implementations and the discussion on gas usage.

### How to Detect if a Contract Implements ERC-165

1. The source contact makes a `STATICCALL` to the destination address with input data: `0x01ffc9a700000000000000000000000001ffc9a7` and gas 30,000. This corresponds to `contract.supportsInterface("0x01ffc9a7")`.
Copy link
Contributor

@veox veox Feb 20, 2018

Choose a reason for hiding this comment

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

Looks like I was wrong on the padding.

Here's a direct transaction to ENS' public resolver, checking `supportsInterface("0x01ffc9a7"). It has input data:

0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000

P.S. I've sent the tx using geth console and ensutils.js, where resolverContract.supportsInterface() ABI description was changed to contain "constant": false.

2. If the call fails or return false, the destination contract does not implement ERC-165.
3. If the call returns true, a second call is made with input data `0x01ffc9a7000000000000000000000000ffffffff`.
4. If the second call fails or returns true, the destination contract does not implement ERC-165.
5. Otherwise it implements ERC-165.

### How to Detect if a Contract Implements any Given Interface

1. If you are not sure if the contract implements ERC-165, use the above procedure to confirm.
2. If it does not implement ERC-165, then you will have to see what methods it uses the old-fashioned way.
3. If it implements ERC-165 then just call `supportsInterface(interfaceID)` to determine if it implements an interface you can use.

## Rationale

We tried to keep this specification as simple as possible. This implementation is also compatible with the current Solidity version.

## Backwards Compatibility

The mechanism described above (with `0xffffffff`) should work with most of the contracts previous to this standard to determine that they do not implement ERC-165.

Also [the ENS](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-137.md) already implements this EIP.

## Test Cases

Following is a caching contract that detects which interfaces other contracts implement. From @fulldecent and @jbaylina.

```solidity
pragma solidity ^0.4.20;

contract ERC165Cache {
bytes4 constant InvalidID = 0xffffffff;
bytes4 constant ERC165ID = 0x01ffc9a7;

enum ImplStatus { Unknown, No, Yes }
mapping (address => mapping (bytes4 => ImplStatus)) cache;

// Return value from cache if available
function interfaceSupported(address _contract, bytes4 _interfaceId) external returns (bool) {
ImplStatus status = cache[_contract][_interfaceId];
if (status == ImplStatus.Unknown) {
return checkInterfaceSupported(_contract, _interfaceId);
}
return status == ImplStatus.Yes;
}

// Repull result into cache
function checkInterfaceSupported(address _contract, bytes4 _interfaceId) public returns (bool) {
ImplStatus status = determineInterfaceImplementationStatus(_contract, _interfaceId);
cache[_contract][_interfaceId] = status;
return status == ImplStatus.Yes;
}

function determineInterfaceImplementationStatus(address _contract, bytes4 _interfaceId) internal view returns (ImplStatus) {
uint256 success;
uint256 result;

(success, result) = noThrowCall(_contract, ERC165ID);
if ((success==0)||(result==0)) {
return ImplStatus.No;
}

(success, result) = noThrowCall(_contract, InvalidID);
if ((success==0)||(result!=0)) {
return ImplStatus.No;
}

(success, result) = noThrowCall(_contract, _interfaceId);
if ((success==1)&&(result==1)) {
return ImplStatus.Yes;
}
return ImplStatus.No;
}

function noThrowCall(address _contract, bytes4 _interfaceId) constant internal returns (uint256 success, uint256 result) {
bytes4 erc165ID = ERC165ID;

assembly {
let x := mload(0x40) // Find empty storage location using "free memory pointer"
mstore(x, erc165ID) // Place signature at begining of empty storage
mstore(add(x, 0x04), _interfaceId) // Place first argument directly next to signature

success := staticcall(
30000, // 30k gas
_contract, // To addr
x, // Inputs are stored at location x
0x20, // Inputs are 8 byes long
Copy link

Choose a reason for hiding this comment

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

Shouldn't this say "32 bytes"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see the update. It still says 8 byes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

x, // Store output over input (saves space)
0x20) // Outputs are 32 bytes long

result := mload(x) // Load the result
}
}
}
```

## Implementation

This approach uses a `view` function implementation of `supportsInterface`. The execution cost is 586 gas for any input. But contract initialization requires storing each interface (`SSTORE` is 20,000 gas). The `ERC165MappingImplementation` contract is generic and reusable.

```solidity
pragma solidity ^0.4.20;

import "./ERC165.sol";

contract ERC165MappingImplementation is ERC165 {
/// @dev You must not set element 0xffffffff to true
mapping(bytes4 => bool) internal supportedInterfaces;

function ERC165MappingImplementation() internal {
supportedInterfaces[this.supportsInterface.selector] = true;
}

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

interface Simpson {
function is2D() external returns (bool);
function skinColor() external returns (string);
}

contract Lisa is ERC165MappingImplementation, Simpson {
function Lisa() public {
supportedInterfaces[this.is2D.selector ^ this.skinColor.selector] = true;
}

function is2D() external returns (bool){}
function skinColor() external returns (string){}
}
```

Following is a `pure` function implementation of `supportsInterface`. The worst-case execution cost is 236 gas, but increases linearly with a higher number of supported interfaces.

```solidity
pragma solidity ^0.4.20;

import "./ERC165.sol";

interface Simpson {
function is2D() external returns (bool);
function skinColor() external returns (string);
}

contract Homer is ERC165, Simpson {
function supportsInterface(bytes4 interfaceID) external view returns (bool) {
return
interfaceID == this.supportsInterface.selector || // ERC165
interfaceID == this.is2D.selector
^ this.skinColor.selector; // Simpson
}
}
```

With three or more supported interfaces (including ERC165 itself as a required supported interface), the mapping approach (in every case) costs less gas than the pure approach (at worst case).

## Copyright

Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).