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

Conversation

fulldecent
Copy link
Contributor

@fulldecent fulldecent commented Feb 13, 2018

See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md


Reference

Work plan

BELOW IS A COMPLETE COPY OF THE LATEST FULL TEXT PROPOSAL (VERSION 03c67d2) FOR YOUR CONVENIENCE.


Preamble

EIP: <to be assigned>
Title: ERC-165 Standard Interface Detection
Author: Christian Reitwießner <chris@ethereum.org>, Nick Johnson <nick@ethereum.org>, Fabian Vogelsteller <fabian@frozeman.de>, Jordi Baylina <jordi@baylina.cat>, Konrad Feldmeier <konrad.feldmeier@brainbot.com>, William Entriken <github.com@phor.net>
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, 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.

Specification

How Interfaces are Identified

For this standard, an interface is a set of function selectors as defined by the Ethereum ABI. This a subset of Solidity's concept of interfaces 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:

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

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

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: 0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000 and gas 30,000. This corresponds to contract.supportsInterface(0x01ffc9a7).
  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 0x01ffc9a7ffffffff00000000000000000000000000000000000000000000000000000000.
  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 already implements this EIP.

Test Cases

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

pragma solidity ^0.4.20;

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

    function doesContractImplementInterface(address _contract, bytes4 _interfaceId) external view returns (bool) {
        uint256 success;
        uint256 result;
        
        (success, result) = noThrowCall(_contract, ERC165ID);
        if ((success==0)||(result==0)) {
            return false;
        }

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

        (success, result) = noThrowCall(_contract, _interfaceId);
        if ((success==1)&&(result==1)) {
            return true;
        }
        return false;
    }

    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 32 bytes long
                                    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.

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.

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
    }
    
    function is2D() external returns (bool){}
    function skinColor() external returns (string){}
}

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.

@fulldecent
Copy link
Contributor Author

Hello authors, Christian Reitwießner @chriseth, Nick Johnson @Arachnid, RJ Catalano @VoR0220, Fabian Vogelsteller @frozeman, Hudson Jameson @Souptacular, Jordi Baylina @jbaylina, Griff Green @GriffGreen, William Entriken github.com@phor.net

Would you please update your email address here (via PR to this PR or just tell me). Current EIP-X requests email address. So I will delete your username and add any email addresses.

Also, I have not verified who wrote the prior version of 165 that this is based on. If you do not feel you should be listed as an author, please remove your name (via PR to this PR or just tell me).

@fulldecent
Copy link
Contributor Author

An argument could be made that the rationale section should explain why we chose to XOR the function selectors rather than have a response for every single function selector.

@fulldecent fulldecent changed the title A standard for interface detection, fixes #165 A standard for interface detection Feb 13, 2018
@fulldecent
Copy link
Contributor Author

Hello @jbaylina, just wanted to check. Are you still on board with this standard? And would you like to maintain the cache, or are you accepting updated to it?

@fulldecent fulldecent changed the title A standard for interface detection ERC-165 A standard for interface detection Feb 13, 2018
@fulldecent fulldecent changed the title ERC-165 A standard for interface detection ERC-165 Standard Interface Detection Feb 13, 2018
@macalinao
Copy link

I wish we just had typeclasses somehow for this. Would make things much easier.

@veox
Copy link
Contributor

veox commented Feb 14, 2018

Meta:

BELOW IS A COMPLETE COPY OF THE LATEST FULL TEXT PROPOSAL (VERSION 4b7b916).
BECAUSE GITHUB PULL REQUEST COMMENTS RENDER MARKDOWN AND DIFFS DON'T.

There's the "View" button on the "Files changed" tab, linking to the rendered version in your repo. (At least when viewing on a desktop browser.)

Changing the blob in the URL from commit hash to branch name (like in the paragraph above) will link the branch HEAD. You could include that in the PR description to avoid it de-syncing from the actual PR.

@fulldecent
Copy link
Contributor Author

Dear esteemed EIP editors, @cdetrio @Souptacular @wanderer @Arachnid @pirapira @vbuterin @nicksavers:

By the EIP work flow process, I do hereby respectfully request by this EIP be granted DRAFT status under the number 165 and that you inform me of the next step to take in this voyage.

Executive summary

Thank you,
William Entriken

@fulldecent
Copy link
Contributor Author

@veox Thank you, I'm using that now!

@fulldecent fulldecent mentioned this pull request Feb 15, 2018
14 tasks
Copy link
Contributor

@nicksavers nicksavers left a comment

Choose a reason for hiding this comment

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

ACK to merge as Draft

@chriseth
Copy link
Contributor

Please note that Solidity contracts will check that the calldata is long enough in the future. This means that you should pad the data to pull 32 bytes instead of just supplying the first 4 bytes of a bytes4 argument.

@chriseth
Copy link
Contributor

I'm not sure if the implementation of noThrowCall is the best. Why does it discard the return value of the call opcode? Furthermore, since input and output are at the same memory position, there will be a return value even in the case of failure.

To avoid more false positives, you should use a separate area for the output and also check that the size is correct using returndatasize.

@fulldecent
Copy link
Contributor Author

@chriseth OK, now I see.

Sorry, the assembly needs more comments. Here are the cases I considered:

  • Sending to a non-existing account
  • The call failing (address is still on the stack)
  • The call succeeding

An all these cases, the discrimination between these two cases is the determinant:

    if (noThrowCall(_contract, InvalidID)) return ImplStatus.No;
    if (!noThrowCall(_contract, ERC165ID)) return ImplStatus.No;

Originally we pulled the contact code length and checked the result of the CALL operation. But none of the matters because of these two cases.

Can you think of a case where the current implementation does not work?

@chriseth
Copy link
Contributor

I just think this is bad practice. You are treating a failed call the same as a call that returns true. We only have four bytes for the selector, there are just too many collisions. I would even go so far and treat a function that returns dirty data (i.e. anything that has a bit set that is not the lowermost one) as not implementing the interface. So basically if the contract either fails or returns a non-bool on call to supportsInterface, it is treated as not implementing this ERC.

@chriseth
Copy link
Contributor

Oh and extcodesize is probably cheaper than calling the contract to check if it exists or not.

Ah and a final remark: Please only adopt this ERC with staticcall instead of call, otherwise it can use reentrant calls and other messy things with the 30000 gas.

@chriseth
Copy link
Contributor

Ah and a post-final remark: If you provide 30000 gas, the called contract can access and modify state, so the function call is not even guaranteed to return the same value on the same call (which might not even be the case with staticcall because of tx.gas - though I'm not sure about that).

@fulldecent
Copy link
Contributor Author

Just implemented staticcall. Now looking to revert to the clearer cache design.

EIPS/eip-165.md Outdated
/// use less than 30000 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.

EIPS/eip-165.md Outdated

### How Interfaces are Identified

For this standard, an *interface* is a set of [function selectors as calculated in Solidity](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 define return types, mutability and events.
Copy link
Contributor

Choose a reason for hiding this comment

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

Note that function selectors are not specific to Solidity, they are part of the ABI specification.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated: "function selectors as defined by the Ethereum ABI"

@Agusx1211
Copy link
Contributor

FYI: The current example on how to read an ERC165 contract is broken, #1640

@wighawag
Copy link
Contributor

wighawag commented May 13, 2019

FYI: The example has another issue: it does not check whether enough gas is passed in the call to supportsInterface which is allowed to use up to 30,000 gas as per the spec.

Indeed due to the behavior of EIP-150 and the fact that STATIC_CALL (and other CALL... opcodes) use the gas value provided only as a maximum (instead of a strict value) it is possible for the following to happen:

  • supportsInterface is called with less than 30,000 gas, because for example
    a) the gas left at the point of call is 30475, which means (because of EIP-150) the gas sent is actually 30475- Math.floor(30475/64))) = 29999
    b) the gas left at the point of call is 10157, which means the gas sent is actually 10157- Math.floor(10157/64))) = 9999
    ...
  • the supportsInterface in question requires more than what is passed, for example 10,000 gas (it might be unlikely but it is currently allowed by the spec)
  • supportsInterface throw for "out of gas" even though it would have returned true if it was given enough gas.
  • doesContractImplementInterface interpret this as : the contract does not implement the interface
  • enough gas is left after the call to complete the transaction : for a) this would be 476 and for b) 158, both values enough to complete a transaction if nothing else need to be executed.

There are different ways to fix it. They are described in #1930 which itself propose a EVM based solution.

  1. check for gasleft() before the call
uint256 gasAvailable = gasleft() - E;
require(gasAvailable - gasAvailable / 64  >= `30000`, "not enough gas provided")
// execute STATIC_CALL with 30000 gas

this require to compute E (the gas between gasleft() and the CALL including what is required to setup (memory use, etc...) and execute the opcode) accurately which might be possible for EIP-165 since it follow a predictable pattern,
This is unfortunately too fragile as gas pricing can change. An over estimation could be used though.

  1. check for gasleft() after the call
// execute STATIC_CALL with 30000 gas
require(gasleft() > 30000/63, "not enough gas left");

This works because if the call throw because of not enough gas, the amount of gas left will be lower than 30000/63. But this also require for supportsInterface to not have code like require(gasleft() > X) since this in that case, the gas left after the call would be bigger than what it would be if all the gas was used
This does not depend on gas pricing except for the 1/64 behavior of EIP-150

  1. use new opcodes proposed by EIP-1930: call variant with strict gas to ensure a specific amount is sent #1930

For EIP-165 there is yet another option:

  1. we could also reduce the maximum allowed gas (currently 30,000) to X where we could ensure that Math.floor(X/64) is never enough to complete a transaction but this is quite fragile as it depends on the specific gas pricing involved. It also require to check whether there is no contract in the wild that actually use more than X gas, as reducing the max gas allowed would break these.
    In any case, it would be good to compute what is the maximal safe value for X with current gas pricing to give an idea of the effect of this issue on EIP-165 contracts in the wild. It is at least as low as 10000.

The best option is in my opinion 3), the solution proposed by #1930 itself since it would be independent of current gas pricing. It would be great to get this in in the next hardfork, or at least get the conversation going since this is an issue affecting other use-case like meta-transaction (see safe-global/safe-smart-account#100)

Note that the bug is present elsewhere (and I expect it to be present in most if not all ERC165 checker, since the specific behavior of EIP-150 is rarely considered) :

see openzeppelin here

or EIP-1820 here

I have some test code here showing the issue

There is also a specific example here involving #1820 which has the same issue

@veox
Copy link
Contributor

veox commented May 13, 2019

FYI: The example has another issue: it does not check whether enough gas is passed in the call to supportsInterface which is allowed to use up to 30,000 gas as per the spec.

I fail to see how this is an issue. This EIP requires that a call to supportsInterface() finish if given 30k gas; if it doesn't, then we may say that "the implementation does not conform to EIP-165", and that's that.

In the examples, you consider cases where the call was not given 30k gas; how supportsInterface() behaves in such conditions is undefined - out of scope as far as EIP-165 is concerned.

If a particular implementation of supportsInterface() wants to check that it got enough gas - that's fine, and up to the impl-n.


The whole point gets even more moot once you consider that the called contract might have pizza_mandate_apology(uint256) implemented instead.

Out of the ways to "fix it" that you propose, (1) and (2) seem most appropriate to me, and can be implemented at language-level (no need for low-level support in EVM).

@wighawag
Copy link
Contributor

I fail to see how this is an issue. This EIP requires that a call to supportsInterface() finish if given 30k gas; if it doesn't, then we may say that "the implementation does not conform to EIP-165", and that's that.

I am not saying the problem is the EIP spec,
I am saying that the example implementation of the ERC-165 checker provided here is broken since it does not check if 30k was actually given.

doesContractImplementInterface should throw if it was not given enough gas to provide 30k gas to the supportsInterface calls

In the examples, you consider cases where the call was not given 30k gas; how supportsInterface() behaves in such conditions is undefined - out of scope as far as EIP-165 is concerned.

Yes this was to illustrate the behavior of the buggy implementation : it allows giving less than 30k gas while it should revert the call at that point. A proper ERC-165 checker implementation would not let that happen.

Out of the ways to "fix it" that you propose, (1) and (2) seem most appropriate to me, and can be implemented at language-level (no need for low-level support in EVM).

The problem with 1) is that
a) it require to compute what amount of gas is required for setting up and performing the call itself.
b) it depends on gas pricing that can change

The problem with 2) is that it only works if the call is not reverting based on a low of gas, as in this case, the validity of the check does not stand anymore.
We could add a restriction to ERC-165 spec though: "supportsInterface should not revert based on gas amount provided, it can throw though (as if all gas is spent, the check works too)"

But, such issues are not only affecting ERC-165. meta-transaction are also affected as illustarted in the gnosis safe issue linked above. And these requires more complicated gas prediction. An EVM solution is the proper way to handle this.

@fulldecent
Copy link
Contributor Author

Please post a test case (preferably pastable into Remix IDE) which shows a compliant implementation of the ERC that produces incorrect results in the supportsInterface() function.

Since this is a long-ago finalized and oft-referenced ERC I request please that the threshold for reopening a discussion will be a working test case and not just discussion points.

@wighawag
Copy link
Contributor

@fulldecent
I already had a test case linked above (https://github.com/wighawag/ethereum_gas) but I added some more test as the previous ones were using a variant of openzepelin implementation.

https://github.com/wighawag/ethereum_gas/blob/1e95dfc787db1d4a1b6600fe054761f5e48bcb50/test/testERC165StandardExample.js

Also since the example stated in ERC-165 is declaring doesContractImplementInterface as external I added a test reflecting such use. : https://github.com/wighawag/ethereum_gas/blob/1e95dfc787db1d4a1b6600fe054761f5e48bcb50/test/testERC165StandardExampleViaCall.js

The bug is still present but because it use the 3 calls the amount of gas where is fails is arround 20000
But I did not try all possibilities. I wonder for example if it could throw at line 11 (first noThrowCall) with less gas.

@wighawag
Copy link
Contributor

wighawag commented May 15, 2019

Another thing I realised is that ERC165 spec does not allow supportsInterface to call other supportsInterface (unless they can be trusted to not use too much gas) which to me sounds like a potential valid use.

This is because at that point of the inner supportsInterface you can't guarantee you'd have 30,000 gas available, since the spec only guarantee it for the first call to supportsInterface

This issue is about the actual spec and should probably be mentioned

@cag
Copy link
Contributor

cag commented May 30, 2019

No, it's not buggy. Those test cases are invalid.

From ERC165 (emphasis mine):

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

The standard is not broken. OpenZeppelin's implementation is not wrong. Technically, yes, a contract making the call to the ERC165 contract should static call with 30,000 gas. That has nothing to do with this implementation. All it means is instead of this:

foo.supportsInterface(0x1234abcd)

you can do this:

foo.supportsInterface.gas(30000)(0x1234abcd)

@fulldecent
Copy link
Contributor Author

At the moment it is not immediately obvious to me that the standard is broken. And "immediately obvious" means a reference to a certain sentence in the standard and a minimal test case that anybody can paste into Remix showing this sentence is invalid/unenforceable.

In the meantime I am happy to continue the discussion at https://github.com/wighawag/ethereum_gas. This is good analysis from a hardworking individual and I am happy to study further there. Also, @wighawag Ronan has some other good projects I am checking out.

I hope we can move this thread to that repo for now. If a minimal test case can be perfected then let's bring it back to this audience at that time.

@wighawag
Copy link
Contributor

I made into a remixable test case. At least as far the ERC165 example implementation (used as a contract to call externally) is concerned

I could not make it work as a remix gist url but it should not be hard to recreate it from the gist here : https://gist.github.com/wighawag/9404f81a2a6d0cfa2134549f186d4d46

The inline comments explain the step to reproduce the issue

@cag

The standard is not broken.

No the standard is not broken, I am just saying the example implementation is.
But I think most people would agree that 30,000 gas was overkill. Now if we can prove that no contract in the wild is using that much gas and we can find a lower gas value that do not exhibit the issue discovered here, we could simply edit the standard to reduce the gasLimit to that value and all is good

OpenZeppelin's implementation is not wrong.

It is, see for further discussion : OpenZeppelin/openzeppelin-contracts#1750

foo.supportsInterface.gas(30000)(0x1234abcd)

If you call it that way you are actually fine, since a throw would be propagated and the caller will also throw. But this cannot be used in all case since you might not want to throw there. You might want to have a different execution path. And that is what the example implementation is allowing you. It gives you a boolean. But as I showed, the example implementation return the wrong boolean in some cases.

Please note that part of the problem is that specifying the gas as part of call like .gas(30000) does not give 30,000 gas to the call. It simply limit the gas sent to be at max 30,000. That's it.
So if there is less than 30,000 gas at the point of the call (actually because of EIP-150, less than 30,000 * 64/63) the amount of gas given will be less than 30,000. And again thanks to EIP-150, there will be at least Math.floor(<gas at the point of call> / 64) left after the call, which can be enough for the call to succeed.

In your example that last part is not possible since the throw is propagated, but in the example implementation which use low level calls, the call to supportsInterface can throw with an out of gas while the call continue, disregarding the result of supportsInterface which could have been a true

@cag
Copy link
Contributor

cag commented May 31, 2019

You might want to have a different execution path.

I suspect the cleanest way to do that would be combining the .gas style with ethereum/solidity#1505 but that isn't here. Otherwise, you'd have to do a staticcall with a gas limit set, but right now, the only way to do that is via assembly. Anyway, that's all on the caller, not the 165 implementation.

Please note that part of the problem is that specifying the gas as part of call like .gas(30000) does not give 30,000 gas to the call. It simply limit the gas sent to be at max 30,000. That's it.

I disagree with your interpretation of the standard. To me, it seems that limit was placed to "pin the blame on someone" if the obvious implementation causes an OOG error. I don't think the authors intended to make it so that 30,000 gas is required to be available during the execution of the staticcall, just that any given call doesn't use more than 30,000 gas. There shouldn't be a need to modify the implementation in a way where it has to do some gas calculation on gasleft, then reject if there wasn't enough gas available. That's just convoluted for no good reason. If the standard really did mandate that, then I'd say the standard is broken.

@wighawag
Copy link
Contributor

wighawag commented Jun 1, 2019

Anyway, that's all on the caller, not the 165 implementation.

That what I meant all along. The bug is in the ERC165Query implementation example. Please have a look at the test cases.

I disagree with your interpretation of the standard. To me, it seems that limit was placed to "pin the blame on someone" if the obvious implementation causes an OOG error. I don't think the authors intended to make it so that 30,000 gas is required to be available during the execution of the staticcall, just that any given call doesn't use more than 30,000 gas. There shouldn't be a need to modify the implementation in a way where it has to do some gas calculation on gasleft, then reject if there wasn't enough gas available. That's just convoluted for no good reason. If the standard really did mandate that, then I'd say the standard is broken.

I am not sure I understand all what you are saying but it seems you say that the following statement from ERC-165 does not mean what it says:

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

Hard to interpret the meaning differently but I agree that the standard is misleading since it also mention :

The source contract makes a STATICCALL to the destination address with input data: 0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000 and gas 30,000. This corresponds to contract.supportsInterface(0x01ffc9a7).

which could be interpreted as passing the 30,000 gas to the call opcode (which does not ensure that 30,000 gas is actually given). I guess the authors did not realise the implication of EIP-150 and the fact that the gas value passed to the call opcode is a max value only.

The problem is that if it was the case that " This function must return a bool and use at most 30,000 gas." did not mean what it means but simply meant "do not give more than 30,000 when calling the function" the standard would be underspecified.

A limit is indeed required for supportsInterface implementations since without it we would not know if the throw results from lacking gas or an exception (because it does not implement the function)

In that case too, the current ERC165Query would be invalid as the test cases show :

A valid implementation of `supportsInterface use 22000 gas and return true
The caller does not give enough gas,
=> it wrongly deduce it does not implement the interface

@wighawag
Copy link
Contributor

wighawag commented Jun 1, 2019

I suspect the cleanest way to do that would be combining the .gas style with ethereum/solidity#1505 but that isn't here. Otherwise, you'd have to do a staticcall with a gas limit set, but right now, the only way to do that is via assembly.

Assembly, or a new feature in solidity (that would be dependent on current opcode pricing to not increase), or as I suggest new opcodes (to later replace the current ones) : #1930

But for ERC-165 there is an alternative way that does not require assembly, as I mentioned earlier, check the gas after the call.

// execute STATIC_CALL with 30000 gas
require(gasleft() > 30000/63, "not enough gas left");```

Just to be on the safe side, this would probably require to stipulate in the standard that supportsInterface must not revert based on the gas amount (require(gasleft()>X) as this would make it impossible to know whether it did not have enough gas. But that would be weird for a supportsInterface implementation to do that anyway

@re-minder
Copy link

re-minder commented Jul 29, 2021

Hey guys, I'm a little new here, so I may not know something about the rules of the discussions here.
I've just read the EIP-165 and I think that some corrections should be made to it in terms of gas prices.
I saw that the last change to the document was introduced in the beginning of 2019. But there was the EIP-2929 that changed the price of the SLOAD opcode when the slot is accessed for the first time in a transaction. Gas prices are not correct right now and, which is even worse, there is the conclusion in the end in which it is told that the view approach with mappings is more gas-efficient than the pure approach with manual calculations in the case of 3+ interfaces implemented. It is completely not true and is misleading the new users.
So I think that the new version of the specification should be made. I could make a pull request if I knew some automation tools for calculating the gas cost of the function.

@fulldecent
Copy link
Contributor Author

@re-minder Thank you! Yes, you are correct--the ERC is now outdated and the conclusion is incorrect based on changes to gas prices.

165 is a rare ERC in that it actually does really care about the gas prices, so your note is very welcome here.

The specification stays the same, although implementation notes are outdated. I am guessing that it is probably appropriate to correct/update this EIP rather than publishing a new one (again, because no specification is changing).

To calculate gas prices, you cane use Remix, compile (with optimization), post a transaction, and use the debugging window (at bottom), and click details (the down arrow next to debug) to get exact answers. Another alternative is to read the Yellow Paper and manually add any parts up.

Would you like to take a stab at the PR here?

@re-minder
Copy link

@fulldecent thank you very much for noting ways of calculating prices, I will use Remix and make a PR very soon.

enjin-io pushed a commit to enjin-io/EIPs-1 that referenced this pull request Nov 13, 2023
enjin-io pushed a commit to enjin-io/EIPs-1 that referenced this pull request Nov 13, 2023
@ilya-korotya
Copy link

This standard works well for input parameters. But how do I check if the method returns the data types I expect, the method signature in solidity does not include the parameters that are returned. And from the point of view of solidity

getCarId(string carName) string
getCarId(string carName) uint256

These are the same interfaces. From the point of view of an application that communicates with solidity, these are completely different interfaces.

@fulldecent
Copy link
Contributor Author

Hi @ilya-korotya I'm happy to help you with that. Please open an issue on Stack Overflow and ping me there. Or add it to the agenda on the weekly Community Service Hour call and we can discuss live.

We try not to do tech support in these GutHub issues when the standard is finalized.

@ilya-korotya
Copy link

Hi @fulldecent . Thank you for your reply. I've created a new question in Stack Overflow. Could you please check it out?

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.