Pseudo-Introspection, or standard interface detection
chriseth opened this issue Β· 61 comments
This EIP is being further developed as #881
For some "standard interfaces" like the token interface ( #20 ), 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 interfaced with. Specifically for #20, a version identifier has already been proposed. This proposal wants to generalize the concept of interfaces and interface versions to interface identifiers:
Every "standard interface" should be assigned a unique identifier (the sha3 hash of its source representation would be suitable) as a bytes32 value. Interfaces that support pseudo-introspection should provide the following methods. Note that this is especially targeted at contracts that can also provide multiple non-conflicting interfaces. This is useful if e.g. a standard token contract also allows a "cheque" functionality, which would have its own interface ID, or if the token does not support allowances.
/// @returns true iff the interface is supported
function supportsInterface(bytes32 interfaceID) constant returns (bool);
/// @returns the (main) interface ID of this contract
function interfaceID() constant returns (bytes32);
/// @returns a bit mask of the supported interfaces.
function supportsInterfaces(bytes32[] interfaceIDs) constant returns (bytes32 r) {
if (interfaceIDs.length > 256) throw;
for (uint i = 0; i < interfaceIDs.length; i++)
if (supportsInterface(interfaceIDs[i]))
r |= bytes32(2**i);
}
Example implementation and usage:
contract GenericInterfaceContract {
function interfaceID() constant returns (bytes32);
function supportsInterface(bytes32 _interfaceID) constant returns (bool);
function supportsInterfaces(bytes32[] interfaceIDs) constant returns (bytes32 r) {
if (interfaceIDs.length > 256) throw;
for (uint i = 0; i < interfaceIDs.length; i++)
if (supportsInterface(interfaceIDs[i]))
r |= bytes32(2**i);
}
}
contract SimpleToken is GenericInterfaceContract {
bytes32 public constant interfaceID = 0xf2a53462c853462ca86b71b97dd84af6a2f7689fc12ea917d9029117d32b9fde;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
function totalSupply() constant returns (uint256 supply);
function balanceOf(address _owner) constant returns (uint256 balance);
function transfer(address _to, uint256 _value) returns (bool success);
function supportsInterface(bytes32 _interfaceID) constant returns (bool) {
return _interfaceID == interfaceID;
}
}
contract Token is SimpleToken {
bytes32 public constant interfaceID = 0xa2f7689fc12ea917d9029117d32b9fdef2a53462c853462ca86b71b97dd84af6;
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
function transferFrom(address _from, address _to, uint256 _value) returns (bool success);
function approve(address _spender, uint256 _value) returns (bool success);
function allowance(address _owner, address _spender) constant returns (uint256 remaining);
function supportsInterface(bytes32 _interfaceID) constant returns (bool) {
return _interfaceID == interfaceID || super.supportsInterface(_interfaceID);
}
}
contract C {
function retrieveRemainingAllowance(GenericInterfaceContract c, address owner) returns (uint) {
if (c.supportsInterface(Token.interfaceID)) {
Token t = Token(c);
return Token(c).allowance(owner, msg.sender);
} else if (c.supportsInterface(SimpleToken.interfaceID)) {
return 0;
} else {
throw;
}
}
}
Note:
Since the EVM does not provide any guarantees for the semantics of contracts, any such information returned by other contracts can only be used as a guideline.
Note2:
The convenient access to the interface ID via Token.interfaceID
is not yet supported by solidity ( ethereum/solidity#1290 ).
Credits: This is the result of a discussion with @konradkonrad and @frozeman
This would be particularly useful for ENS resolvers, and would allow removing the has()
method, which implements an equivalent (ENS-specific) operation.
supportsInterfaces
seems like a lot of extra overhead for relatively little gain over just calling the contract multiple times, however, especially given the low number of interfaces one is likely to need to check.
A couple of other suggestions:
interfaceID
assumes the existence of a 'primary' interface, and doesn't seem a-priori terribly useful. Unless there's compelling use cases for it, I'd suggest dropping it.- Making the interface ID the 32 bit XOR of all the function signatures the interface contains would make calculating it simpler, relying only on existing code to generate function hashes and simple arithmetic ops.
Discussing this with Chris in the Solidity channel, he pointed out that an interface is more than its ABI; it's possible for two interfaces with the same set of function signatures to have different expectations of how they operate (especially since function signatures don't include parameter names!). So, the interface ID should be a pseudorandom value with low probability of colliding with any other interface ID; it doesn't need to bear a strict relationship to the source (and arguably, shouldn't, so people don't make assumptions about it being usable as some kind of test).
I agree - so ignore my earlier suggestions about constructing the interface ID from the constituent functions.
Edit: On the other hand, Go does 'duck typed interfaces' and generally doesn't have a problem; although you can have two interfaces with the same ABI but different intents, the chances of both of them being relevant in a given scenario seem vanishingly small. For instance, it's easy to avoid a collision in interface definitions for ENS resolvers, and nobody's going to ask an ENS resolver if it implements, say , a token-related interface.
After some more discussion, I think we did settle on just using the xor of the function identifiers that are part of the interface as a canonical identifier.
@chriseth @Arachnid I am of the belief that we should be either copying how Go does this or how Rust does this in regards to our interfaces. They should be their own type. Something looking like this:
contract C {
interface I {
foo(uint256) returns (uint256);
bar(bool, string) returns (bool);
}
}
contract A {
struct MyStruct {
function foo(uint256) returns (uint256);
function bar(bool, string) returns (bool);
}
MyStruct _myStruct;
D someContract;
function a_call() {
d.f(_myStruct); //passes in _myStruct as interface to D
}
}
contract B {
E _e;
D _d;
function call_D() {
_d.f(_e); //passes in E to D and thus will clear because functions align.
}
}
contract E {
function foo (uint256) returns (uint256) {
//some function
}
function bar (bool, string) returns (bool) {
//some function
}
}
contract D is C {
function f(_interface I) {
switch type(_interface) { //switch on the types of objects passed into the interface (i think this should be mandatory to some extent)
case A:
uint a = _interface.foo(1);
case B:
bool truth = _interface.bar(true, "something");
default: throw;
}
}
}
I'm not sure about xor-ing it...that may work, I'm just not sure I understand it yet, need to look. What I think we can do for the type assertion is to hash for these types based on their actual function opcodes that they are utilizing/generating on the stack portion. This will make it so that we can verify that only these contracts are calling externally and can make the interface quite a useful safety mechanism.
@VoR0220 I think what you're talking about is language design, and thus orthogonal to this EIP, which is mostly about how to implement the feature at the EVM level.
I believe you might be correct. Will reread this in the morning.
Okay. 2 Months later, I have reread it and with some help I think I understand the proposal better. Here's my question. How might we distinguish between different versions of a contract interface? Would that need to be included? Or would it be "Version independent"?
Here are the modifications that I would do in the standard:
1.- I would keep GenericInterface very simple with just one single method: supportsInterfaces(bytes32)
. I would not include the other two methods: supportsInterfaces
and interfaceID
.
2.- This interface should always return true
when called with 0xf1753550
as a parameter. (This is the interface that implements supportsInterfaces(bytes32)
.
3.- This function should always return false
when called with 0xffΒ·Β·Β·ff
parameter.
With this definition, You can test if a contract implements EIP165 or not by trying to call supportInterface
for GenericInterface (Should always return true) and an Invalid interface (Should always return false). If an exception, out of gas or the results are not true/false, then this means that this contract does not implement EIP165
For saving gas, a single generic EIP165Cache contract can exist in the blockchain.
You can see an how this implementation would be here:
https://github.com/jbaylina/EIP165Cache/blob/master/contracts/EIP165Cache.sol
I broadly agree, although I'm not sure it's necessary to be able to test supportsInterface
explicitly like that; if you call it and it throws, it's safe to say the method isn't supported (throwing is the default fallback behaviour for some time now).
@Arachnid I believe that there are some contracts (like wallets or normal addresses) that don't throw. They just do nothing.
In those old contracts, they may return false or true. What I am testing is that they return true in a "must Implement" interface and false in a "must not implement" interface. This way we guaranty that it will work in most (if not all) old contracts.
If you agree on this, I will make some tests with the most used contracts so that they return false. We will see if this "double check" is necessary or not.
@Arachnid Another question, I see that in the ENS implementation, the fingerprint is:
function supportsInterface(bytes4 _interfaceID) constant returns (bool);
instead of
function supportsInterface(bytes32 _interfaceID) constant returns (bool);
I think it's better to use bytes32 instead of bytes4 even if we use only 4 bytes. This will give space for future ampliations.
And may be it's too late now, but I think it would be good to also return true for thesupportsInterface
interface:
0xf1753550 -> supportsInterface(bytes32)
0x01ffc9a7 -> supportsInterface(bytes4)
bytes4
was what was agreed on in the Solidity channel, but @chriseth never updated the draft accordingly. I don't think it's possible to change how many bytes we use without breaking backwards compatibility, so I'd vote for keeping it as bytes4
.
There's certainly no harm in having contracts return true for the supportsInterface
interface. I'll update the ENS contracts accordingly.
Since you're working on this, any interest in submitting a PR that formalises it?
I have the first draft of the Specification Here:
https://docs.google.com/document/d/16AC8gBylYgPE5zr9C4mz2fS1oYLHQf1o8sGjxmw8Juw/edit?usp=sharing
I have also created and tested the EIP165Cache contract Here:
https://github.com/jbaylina/EIP165Cache
Please, comment/correct in the doc before creating the formal PR.
Looks like a good start, but it would be much easier to leave comments against the PR than in a Google Doc (and they'll be preserved for posterity).
Ok. Then I'll Create the PR.
Just deployed the contract in testnet:
https://ropsten.etherscan.io/address/0xb60739c904b6d202c7cb2ba4be0fd4b733374c4e#readContract
I'd love to ensure this is easy to adopt by folks looking at ERC-721, but it's really not clear to me what the right behaviour is with regards to optional functions. I see three possibilities:
- The ERC-165 interface signature includes just the required methods (The sample code at the bottom of the draft spec seems to imply that this is not the expected interpretation, and includes optional methods (like
symbol()
) in the signature calculation for ERC-20.) - The ERC-165 interface signature for a specific implementation includes only those optional functions that are actually implemented. This has the nice properly that you could theoretically figure out which subset of optional functionality each contract implements, but has the absolutely abysmal property that you'd have to query every possible subset of optional functionality in order to test a contract! π¬
- The ERC-165 interface signature for a specific implementation includes all optional functions. This seems a bit weird to me, because it kind of feels like promising to implement functions that you might not implement.
Because the example includes optional methods for ERC-20, I'm going to include optional methods for ERC-721. @jbaylina: Please let me know ASAP if I'm getting this wrong! :-D
@dete This interface does not contemplate optional functions. They are defined or not defined in the interface specification. If you define a function as part of the interface, it is included, if not then not. You may want to define two interfaces one required and one optional, or may be you want to define one interface for each function, or one interface with a function that specifies the capabilities of the implementation, what ever you want.
But this is not part of this standard. This should be part of the specific standard that defines the interface or interfaces.
I see your point.
But at the same time, some guidance is not unreasonable until norms are established. At the very least, the guidance could be essentially what you wrote:
It may be appropriate for single interface standard can define multiple ERC-165 interface signatures to indicate "degrees" of implementation. It is up to each interface standard to determine the appropriate granularity of interface signatures in it's own domain. For example, you could have one interface signature for core functionality, and one signature for an implementation that adds optional functionality. If one interface is a proper superset of another interface, an implementation that supports the more encompassing standard should report support for both.
Straw man proposal: use bool functions to achieve goal of EIP 165.
Implementation example found in the wild:
/// @title SEKRETOOOO
contract GeneScienceInterface {
/// @dev simply a boolean to indicate this is the contract we expect to be
function isGeneScience() public pure returns (bool);
/// @dev given genes of kitten 1 & 2, return a genetic combination - may have a random factor
/// @param genes1 genes of mom
/// @param genes2 genes of sire
/// @return the genes that are supposed to be passed down the child
function mixGenes(uint256 genes1, uint256 genes2, uint256 targetBlock) public returns (uint256);
}
Why not simply add supportsEIP20Basic() public pure returns (bool);
to every basic token and then supportsEIP20() public pure returns (bool);
to full tokens?
Does this straw proposal have a greater or lesser footprint on the EVM than EIP-165?
@fulldecent That wouldn't be very future-proof.
For the question of "does contract C support interface I?", we'd like to be able to get a yes/no
answer, as compared to the current yes/{execute fallback code}
.
Imagine this EIP having been widely adopted. Contract Ali wants to check if contract Bob supports Ali's favourite interface. Ali calls Bob with a standard function (the pre-known 4-byte selector for supportsInterface
). Bob says "no" (false
). Ali continues with the "counter-party doesn't support interface" branch of its code - e.g. emits an event and stops; or tries a different interface; or pings contract Cat as the next candidate counter-party.
Imagine now the isSomethingSpecial
approach. Ali calls Bob, and that falls through to Bob's fallback (because Bob doesn't support that particular interface), which revert
s. Ali has no way to inquire Bob if they can have a civil conversation. (OK, there are ways, but they're messy logic-wise.)
@veox Thank you for the explanation.
We must always be prepared for executing fallback code because targets may not implement EIP-165. However I see the value EIP-165 creates is that checking for n interface compliances would only execute at most one fallback code. Using straw man this could trigger up to n fallback codes.
And of course n grows fast when we start talking about ERC-20Basic, ERC-20, ERC-20v2, ...
@chriseth Will you please update the original issue at the top to include the latest proposal, with bytes4. If you wish to preserve the current text for some reason, you might see how they did it at #721.
Also, can you please include a solidity interface at the top following all best practices so that others can copy paste it. Maybe this would be it.
interface ERC165 {
// Interface signature for ERC-165
bytes4 constant INTERFACE_SIGNATURE_ERC165 = // 0x01ffc9a7
bytes4(keccak256('supportsInterface(bytes4)'));
function supportsInterface(bytes4 interfaceID) constant returns (bool);
}
Security note: it is not very difficult to create collisions in the 32-bit xor "hashing" algorithm here. If you wanted to design an interface that has the same signature as the ERC20 signature then you could do it in a reasonable amount of time.
You could then have other contract authors unwittingly implement your interface. And those contracts would test positive for your interface and for the ERC20 interface.
Anyway, the algorithms are already written to do this type of attack, just look up step two of the quadratic sieve method.
^ If anyone thinks this is a security issue, please share! Then I will have an excuse to find a collision!
Ping @chriseth
@fulldecent I disagree on the pure
part.
There are already examples of upgradable dispatcher/proxy contracts with DELEGATECALL
s to libraries. I'm working on furthering this, to support those as swap-{in,out} modules.
Hard-coding supported interfaces into bytecode would work against this use case.
The fact that the use case breaks caching is, well, unfortunate. Perhaps the assumption that such a response can - or should! - be cached (by an external contract) is misguided in the first place.
EDIT: In either case, the "caching contract" mentioned here should IMO not be considered as part (or in tandem) with this proposal.
It is true that programers will deploy half-baked contracts onto the network. See Cryptokitties. But that's their fault. If a contract advertises compliance with an interface then I expect it to comply with that interface -- today, tomorrow, or whenever I feel like interacting with it.
I know people would love to deploy an empty contract, and then set a delegate contract to do all the processing. That means their contract can have zero accountability and it is not auditable.
In the HTML/Javascript, we look down on that practice and we invented subresource integrity.
Or, said another way:
A contract is free to delegate stuff. But then the delegatee is compliant with the interface, not the delegator.
If a delegator want to be compliant with an interface then it actually has to be compliant with the interface, like all the time.
@fulldecent @jbaylina I'm fine with handing off this EIP to someone else. Others have spent far more time thinking about this than me. I'm not sure github can do transferring issue ownership, but otherwise it might also be a good idea to open a pull request if people think that some form of this is already solidified.
Note that the hash collision issue is also present in function selectors themselves. The shortest example I could find is
contract test {
function gsf() public { }
function tgeo() public { }
}
By the way, I would not consider this too much of a problem for this EIP (it is a problem for auditors and tools have to warn auditors), since what a contract announces to support and how it actually behaves are two different things anyway.
Oh yeah, functions only have four byte signatures!
Cool. Maybe I can help here after ERC721.
Here is some progress. @chriseth do you have an opinion on whether a supportsInterface
should be pure?
Arguments for:
- Caching works
- After you query supportsInterface you can be confident the interface is supported
Arguments against:
- Contract behavior (as far as whether it supports an interface) can't be changed after it is deployed
- supportsInterface can be implemented as an
O(1)
hash, which might be better thanO(n)
if statements.
Progress on a draft. Still has work to do. But I would not want to continue until more discussion on pure
-- because that affects a large part of this proposal.
Preamble
EIP: <to be assigned>
Title: Standard Interface Detection
Author: Christian ReitwieΓner @chriseth, Nick Johnson @Arachnid, RJ Catalano @VoR0220, Fabian Vogelsteller @frozeman, Hudson Jameson @Souptacular, Jordi Baylina @jbaylina, Griff Green @griffgreen, William Entriken @fulldecent
Type: Standard Track
Category ERC
Status: Draft
Created: 2018-01-23
Simple Summary
Creates a standard method to publish and discover what interfaces a smart contract implements.
Abstract
This standard consists of the standardization of the following procedures:
- How a contract will publish the interfaces it implements.
- How to determine if a contract implements ERC-165
- How to determine if a contract implements any given standard interface
Motivation
For some "standard interfaces" like the ERC-20 token interface, 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 wants to stadardize the concept of interfaces and map interface versions to interface identifiers.
Specification
Standard interface identifier
Any standard interface will be identified by a specific bytes4
number. This number will be the XOR of all bytes4
of each method calculated as solidity does.
For example, to calculate the id of a standard interface
totalSupply() -> Method ID: 0x18160ddd
balanceOf(address) -> Method ID: 0x70a08231
transfer(address, uint256) -> Method ID: 0xa9059cbb
InterfaceID = 0x18160ddd ^ 0x70a08231 ^ 0xa9059cbb = 0xc1b31357
EIP165 interface
Any contract that wants to implement a standard interface must also implement this method:
function supportsInterface(bytes4 interfaceID) returns (bool);
This function must return:
true
wheninterfaceID
is0x01ffc9a7
(EIP165 interface)false
wheninterfaceID
is0xffffffff
true
for anyinterfaceID
that this contract implementsfalse
for any otherinterfaceID
Or stated equivalently, this function must return true
if and only if interfaceID
is a supported interface, and the interface 0x01ffc9a7
must be supported and the interface 0xffffffff
must not be supported.
Procedure to determine if a contract implements EIP165 standard
- The source contact makes a
CALL
to the destination address with input data:0x01ffc9a701ffc9a7
value: 0 and gas 30000 - If the call fails or return false, the destination contract does not implement EIP165
- If the call returns true, a second call is made with input data:
0x01ffc9a7ffffffff
- If the call fails or returns true, the destination contract does not implement EIP165
- Otherwise it implements EIP165
Procedure to determine if a contract implements a standard interface
- If you are not sure if the contract implements EIP165 Interface, use the previous procedure to confirm.
- If it does not implement EIP165, then you will have to see what methods it uses the old fashioned way.
- If it implements EIP165 then just call
supportsInterface(interfaceID)
to determine if it implements an interface you can use.
Example implementation
A token
pragma solidity ^0.4.11;
contract EIP165Cache {
function interfaceSupported(address _contract, bytes4 _interfaceId) constant returns (bool);
function eip165Supported(address _contract) constant returns (bool);
function interfacesSupported(address _contract, bytes4[] _interfaceIDs) constant returns (bytes32 r);
}
contract IERC165 {
bytes4 constant INTERFACE_IERC165 =
bytes4(sha3('supportsInterface(bytes4)'));
mapping(bytes4 => bool) public supportsInterface;
function IERC165() {
supportsInterface[INTERFACE_IERC165] = true;
}
}
contract IToken is IERC165 {
bytes4 constant INTERFACE_ITOKEN =
bytes4(sha3('name()')) ^
bytes4(sha3('symbol()')) ^
bytes4(sha3('decimals()')) ^
bytes4(sha3('totalSupply()')) ^
bytes4(sha3('balanceOf(address)')) ^
bytes4(sha3('transfer(address,uint256)')) ^
bytes4(sha3('transfer(address,uint256,bytes)'));
function IToken() {
supportsInterface[INTERFACE_ITOKEN] = true;
}
event Transfer(address indexed from, address indexed to, uint256 value, bytes data);
function name() constant returns(string);
function symbol() constant returns(string);
function decimals() constant returns(uint8);
function totalSupply() constant returns (uint256 supply);
function balanceOf(address _owner) constant returns (uint256 balance);
function transfer(address _to, uint256 _value) returns (bool success);
function transfer(address _to, uint256 _value, bytes) returns (bool success);
}
contract ITokenFallback is IEIP165{
bytes4 constant INTERFACE_ITOKENFALLBACK =
bytes4(sha3("tokenFallback(address,uint256,bytes)"));
function ITokenFallback() {
supportsInterface[INTERFACE_ITOKENFALLBACK] = true;
}
function tokenFallback(address _from, uint _value, bytes _data);
}
contract Token is IToken, ITokenFallback {
EIP165Cache constant eip165Cache = EIP165Cache(0x1234567890123456678901234567890);
mapping (address => uint) _balances;
uint _totalSupply;
function Token(uint _initialSupply) {
_totalSupply = _initialSupply;
_balances[msg.sender] = _initialSupply;
}
// Implementation of ISimpleToken
function name() constant returns(string) {
return "Example Token";
}
function symbol() constant returns(string) {
return "EXM";
}
function decimals() constant returns(uint8) {
return 18;
}
function totalSupply() constant returns (uint256 supply) {
return _totalSupply;
}
function balanceOf(address _owner) constant returns (uint256 balance) {
return _balances[_owner];
}
function transfer(address _to, uint256 _value) returns (bool) {
return transfer(_to, _value, "");
}
function transfer(address _to, uint256 _value, bytes _data) returns (bool success) {
if (_value > _balances[msg.sender]) throw;
_balances[msg.sender] -= _value;
_balances[_to] += _value;
if ( eip165Cache.eip165Supported(_to) ) {
if (eip165Cache.interfaceSupported(_to, ITokenFallbackID)) {
ITokenFallback(_to).tokenFallback(msg.sender, _value, _data);
} else {
throw;
}
}
Transfer(msg.sender, _to, _value, _data);
return true;
}
// Implementation of ITokenFallback
function tokenFallback(address , uint , bytes ) {
throw; // The token contract should not accept tokens.
}
}
An ERC165 Cache
See https://github.com/jbaylina/EIP165Cache.
It is beneficial to have a canonical cache on each network. You are welcome to use the following deployed caches:
- Mainnet: xxxxx ADD ADDRESS HERE
- ⦠other nets⦠LINK TO ETHERSCAN WITH
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 should work in most of the contracts previous to this standard to determine that day does not implement EIP165.
Also the ENS already implements this EIP.
Test Cases
To be tested
Implementation
https://github.com/jbaylina/EIP165Cache
Copyright
Copyright and related rights waived via CC0.
My previous comment on view
/pure
was that this is a compile-time distinction. It may be unenforceable at runtime (at least ATM). It's also Solidity-centric.
In the draft, is the example token supposed to be ERC20-compliant?.. There's no indication to that effect, and it does lack transferFrom(address,address,uint256)
, so I'd guess no; it would be nice, however, to explicitly state this fact somewhere ("A (non-ERC20) token"), so there's less chance someone makes an incorrect assumption.
view
will be enforced using staticall
soon.
@chriseth thanks for the note. Currently we are discussing the distinction between view
and pure
. This is not impacted by staticcall
.
The cache could have a public function queryContract(address, bytes4)
that bypasses cache and re-queries contract. This allows contracts that change their interface to eventually update caches.
There will be a period with stale data in the cache. In the case of a contract adding support for an interface, this should not matter much as the contract works as advertised by the cache. New functionality will become available eventually. In case of removing an interface, it can cause problems, but I don't think contracts removing an interface should have expectations that this will be a smooth transition.
Also:
- a privately controlled cache could allow the operator to override the cache. The operator can then, for example, flag ERC20 support on non-EIP165 tokens.
- a more adventurous cache could test the existence of
balanceOf
,totalSupply
,allowance
and speculatively flag support for ERC20Basic / ERC20.
@fulldecent In your token example you only claim support for ERC223, but it also supports ER20Basic. It would better illustrate the point of this EIP if it flags support for more interfaces:
bytes4 constant EIP20Basic =
bytes4(keccak256('totalSupply()')) ^
bytes4(keccak256('balanceOf(address)')) ^
bytes4(keccak256('transfer(address,uint256)'));
bytes4 constant EIP20Optional =
bytes4(keccak256('name()')) ^
bytes4(keccak256('symbol()')) ^
bytes4(keccak256('decimals()'));
bytes4 constant EIP20 =
EIP20Basic ^
bytes4(keccak256('allowance(address,address)')) ^
bytes4(keccak256('approve(address,uint256)')) ^
bytes4(keccak256('transferFrom(address,address,uint256)'));
bytes4 constant EIP223 =
EIP20Basic ^
EIP20Optional ^
bytes4(keccak256('transfer(address,uint256,bytes)'));
function IToken() {
supportsInterface[EIP20Basic] = true;
supportsInterface[EIP20Optional] = true;
supportsInterface[EIP223] = true;
}
(Alternatively it could be done by declaring more interface contracts and using inheritance.)
@recmo. Thank you, I will work this into the next version.
@fulldecent I've written a bunch already (source). I went with your inheritance approach.
@ALL: Would it make sense to have a canonical repository for these interface contracts somewhere? Perhaps as part of the ethereuem/EIPs
repo. These interface contracts are currently being copy-pasted to all sorts of places, and different conflicting version are floating around. With EIP165 it will be especially important to be consistent, or else the identifiers will differ. It would also be nice to have them with full natspec documentation.
Long term it would be nice if 1) these canonical interfaces also define semantic requirements like 'sum of balances equals totalsupply' and 2) EIP165 becomes part of the ABI and Solidity gains native support for it; for example by auto generating the correct supportsInterface
method.
Right now we have a few solutions for what we are trying to solve:
165-VIEW | 165-PURE | 820 | |
---|---|---|---|
Can express if you implement an interface | YES | YES | YES |
Can change if you implement an interface | YES | NO | YES |
Can accurately cache results with a singleton contract | POSSIBLE | YES | YES |
Can accurately cache results without a singleton contract | NO | YES | NO |
Can delegate compliance to a separate contract | NO | NO | YES |
Uses function signatures as interface | YES | YES | NO |
Uses string as interface | NO | NO | YES |
Allows third-party to change interface implementedness | POSSIBLE | NO | YES |
VERSION 3: Added assembly parts
Also, we could just be a boss and do actual introspection on the blockchain from a contract.
- This only works from Solidity
- This exploits the fact that Solidity inserts function dispatch boilerplate at the beginning of all contracts
- Function dispatch knows of all defined functions
- This can return false negatives if the contract has some fancy assembly in its fallback function (i.e. it's own function dispatch)
pragma solidity ^0.4.19;
contract ContractIntrospector {
enum Result {No, Yes, PleaseCallAgainToContinueSearching}
mapping (address => mapping(bytes4 => bool)) private contractFunctionSelectorSupported;
mapping (address => bytes4[]) public contractFunctionSelectors;
mapping (address => uint) private searchProgress;
mapping (address => bool) public doneSearching;
function getFunctionSignature(string _function) external pure returns (bytes4) {
return bytes4(keccak256(_function));
}
// Save lookup table in memory
function getBytesForOpcodes() private pure returns (uint256[256] retval) {
// Initialize bytesForInstruction
// Read the Yellow Paper, find Ξ± for each opcode
for (uint i = 0; i < 256; i++) {
retval[i] = uint256(-1); // Default return value
}
retval[0x00] = uint256(-1); // HALT
retval[0x01] = 1 + 32*1; // ADD
retval[0x02] = 1 + 32*1; // MUL
//...
}
function getSize(address _addr) public view returns (uint256 _size) {
assembly {
_size := extcodesize(_addr)
}
}
function getCode(address _addr, uint256 _start, uint256 _length)
public
view
returns (bytes _code)
{
_code = new bytes(_length);
assembly {
extcodecopy(_addr, add(_code, 0x20), _start, _length)
}
}
function howManyBytesCanWeProcessWorstCaseWithRemainingGas()
private
returns (uint256 _size)
{
//TODO: implement this better
return 20;
}
function doesContractSupportFunctionSelector (
address _addr,
bytes4 _functionSelector
)
public
returns (Result)
{
if (contractFunctionSelectorSupported[_addr][_functionSelector]) {
return Result.Yes;
}
uint contractSize = getSize(_addr);
uint start = searchProgress[_addr]; // starts at zero
if (start >= contractSize) {
return Result.No;
}
uint256[256] memory bytesForOpcode = getBytesForOpcodes();
uint length = howManyBytesCanWeProcessWorstCaseWithRemainingGas();
bytes memory code = getCode(_addr, start, length);
uint ptr = 0;
while (ptr < length) {
if (code[ptr] == 0x63) { // PUSH4
bytes4 selector = code[ptr+1] | // TODO: is endian here correct?
code[ptr+2] << 8 |
code[ptr+3] << 16 |
code[ptr+4] << 24;
contractFunctionSelectorSupported[_addr][selector] = true;
contractFunctionSelectors[_addr].push(selector);
if (selector == _functionSelector) {
searchProgress[_addr] = start + ptr;
if (start + ptr > contractSize) {
doneSearching[_addr] = true;
}
return Result.Yes;
}
}
if (code[ptr] == 0x5b) { // JUMPDEST
searchProgress[_addr] = uint256(-1);
doneSearching[_addr] = true;
return Result.No;
}
ptr += bytesForOpcode[uint256(code[ptr])];
}
if (ptr >= length) {
searchProgress[_addr] = uint256(-1); // the maximum uint value
return Result.No;
}
return Result.PleaseCallAgainToContinueSearching;
}
}
Yeah, this is mostly a joke/though experiment. But maybe you were thinking it too, so just wanted to say it.
Haha, yes, I non-seriously considered decompiling too. (Btw, the current selector prequel has a gas cost linear in number of functions, but low constant cost is quite doable using a map, but would break decompiling approaches.)
Just to be clear. With 'heuristics' in my earlier comment I meant something similar to @jbaylina's noThrowCall
to check for the existence of never-throw functions like owner
, balanceOf
, allowance
, name
, etc. I think such a heuristic would be able to distinguish the many of the current interfaces that don't support EIP165 yet.
I'm not a big fan of EIP-820. 1) It complicates deployment, which will complicate development and testing and hurt adoption. This standard is pretty much code-copy paste to implement, easy for adoption. 2) EIP820 in its current form relies on user-provided strings for ids. I foresee contracts incorrectly claiming to support the ERC20 interface. 3) It allows for other contracts to implement an interface on behalf of the contract, this adds an extra layer of complication to using interfaces on contracts as you now need to support a layer of indirection. This indirection can change at any moment, so you can not cache it. This seems like a very big gas cost overhead to me. In fact, this indirection can change mid-transaction. Finally, what happens if a malicious token contract 'claims' that its 'transfer' interface is implemented by an unrelated valuable token?
My favorite is 165-view. The only problematic caching case is when a contract drops support for a interface. (Adding can be solved by manually invalidating cache, which is trustless) But this is already a problematic case without EIP165. Contracts that plan to do this should say so in advance (lest developers will depend on an interface) and when they plan in advance they can also say not to rely on interface caches.
Quick updates:
- I need to hear from somebody who would vote on this ERC-165 as to their thoughts on which direction we should take.
- That gives me a reasonable hope that my effort here can result in a win.
- We can harmonize interface names with #820
- Maybe we should separate interface names from XOR function selectors
- I built an awesome Solidity decompiler, almost works. Does introspection, not pseudo
- Use this.f.selector. It is way better
OK. I am going to go forward with writing this up for EIP165-VIEW variant. PR coming.
@recmo Sorry, that link broke, can you please send again?
Hello channel. #881 is very far along. I have described it faithfully as was discussed here ("165-VIEW" as above). As yet, I have seen no controversy in the 165-VIEW approach.
Previously there was an inconsistency with the 165-VIEW approach and the EIP165Cache from @jbaylina. I have resolved this by rewriting the cache.
Previously there was an open item from 165-PURE discussion about the gas cost of using an interface mapping versus using the PURE approach. Both implementations are shown and the gas costs are discussed.
Please support me by going to #881 and reacting with a party emoji π if you believe this EIP should be accepted. Acceptance is NOT final. The significance of acceptance is detailed in EIP-1:
If the EIP collaborators approve, the EIP editor will assign the EIP a number (generally the issue or PR number related to the EIP), label it as Standards Track, Informational, or Meta, give it status βDraftβ, and add it to the git repository.
The supporting documentation for RJ @VoR0220, Hudson @Souptacular and Griff @GriffGreen
being listed as authors in this standard are inside your Google Doc version at https://docs.google.com/document/d/16AC8gBylYgPE5zr9C4mz2fS1oYLHQf1o8sGjxmw8Juw/edit#heading=h.kkpgop5ftnhc
I do not have access to that history. FILE-> VERSION HISTORY -> PRIOR VERSIONS.
Would you please review this history and attest that these three individuals should be listed as authors?
If you recommendation a deletion, please suggest it here and at #881. If you can make the attestation, this will be helpful to document for posterity.
The PR for this in #881 has been merged
FYI: On the NEO blockchain, this requirement was solved in the following way: https://github.com/neo-project/proposals/blob/master/nep-10.mediawiki
There is a bug in the code in this EIP caused by reading from reused memory that is not zeroed out.
success := staticcall(
30000, // 30k gas
_contract, // To addr
x, // Inputs are stored at location x
0x24, // Inputs are 36 bytes long
x, // Store output over input (saves space) <-----
0x20) // Outputs are 32 bytes long
result := mload(x) // <-----
If the call reverts, or if it succeeds but does not return anything (e.g an EOA), x
will contain erc165ID
. I believe it will also contain a bad value if the call returns less than 32 bytes. This results in an incorrect doesContractImplementInterface
implementation because result
is compared as a number with 1
and 0
.
OpenZeppelin's implementation does not have this bug. Has anyone deployed the code in the EIP?
Thanks for sharing. So if the call returns less that 32 bytes wont that just only lay down the bytes that returned? ERC-165 is pretty extensive with the three-part test. I can't think of a case that would break that implementation when an unexpected short response is produced.
Also interested to hear if anybody is use the reference code as-is.
In general, I will ask please that if allegations of a EIP are raised after final status, that they include runnable test cases. Preferably in a minified repository that also runs with Travis CI or similar.
It's true that the three-part test mitigates the bug in a way that it doesn't seem to have any practical consequences.
Is interface detection implemented automatically, or does registration have to manually occur?