-
Notifications
You must be signed in to change notification settings - Fork 12k
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
Add ERC165Query library #1086
Merged
Merged
Add ERC165Query library #1086
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
e226781
Add ERC165Query library
ldub bf13df9
Address PR Comments
ldub c9eef38
Add tests and mocks from #1024 and refactor code slightly
ldub 9babb73
Fix javascript and solidity linting errors
ldub bfee4e0
Split supportsInterface into three methods as discussed in #1086
ldub 3176956
Change InterfaceId_ERC165 comment to match style in the rest of the repo
ldub 1a09d88
Fix max-len lint issue on ERC165Checker.sol
ldub 1ec887e
Conditionally ignore the asserts during solidity-coverage test
ldub 53c3c22
Switch to abi.encodeWithSelector and add test for account addresses
ldub d3a42be
Switch to supportsInterfaces API as suggested by @frangio
ldub 2535886
Adding ERC165InterfacesSupported.sol
ldub 2b48005
Fix style issues
ldub 92d7869
Add test for supportsInterfaces returning false
ldub f0829ad
Add ERC165Checker.sol newline
ldub f62067b
Merge branch 'master' into add_erc165_library
shrugs 111d588
feat: fix coverage implementation
shrugs 9de7979
fix: solidity linting error
shrugs 0b08abc
fix: revert to using boolean tests instead of require statements
shrugs f6d8063
fix: make supportsERC165Interface private again
shrugs e228f81
rename SupportsInterfaceWithLookupMock to avoid name clashing
frangio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
pragma solidity ^0.4.24; | ||
|
||
|
||
/** | ||
* @title ERC165Checker | ||
* @dev Use `using ERC165Checker for address`; to include this library | ||
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-165.md | ||
*/ | ||
library ERC165Checker { | ||
// As per the EIP-165 spec, no interface should ever match 0xffffffff | ||
bytes4 private constant InterfaceId_Invalid = 0xffffffff; | ||
|
||
bytes4 private constant InterfaceId_ERC165 = 0x01ffc9a7; | ||
/** | ||
* 0x01ffc9a7 === | ||
* bytes4(keccak256('supportsInterface(bytes4)')) | ||
*/ | ||
|
||
|
||
/** | ||
* @notice Query if a contract supports ERC165 | ||
* @param _address The address of the contract to query for support of ERC165 | ||
* @return true if the contract at _address implements ERC165 | ||
*/ | ||
function supportsERC165(address _address) | ||
internal | ||
view | ||
returns (bool) | ||
{ | ||
// Any contract that implements ERC165 must explicitly indicate support of | ||
// InterfaceId_ERC165 and explicitly indicate non-support of InterfaceId_Invalid | ||
return supportsERC165Interface(_address, InterfaceId_ERC165) && | ||
!supportsERC165Interface(_address, InterfaceId_Invalid); | ||
} | ||
|
||
/** | ||
* @notice Query if a contract implements an interface, also checks support of ERC165 | ||
* @param _address The address of the contract to query for support of an interface | ||
* @param _interfaceId The interface identifier, as specified in ERC-165 | ||
* @return true if the contract at _address indicates support of the interface with | ||
* identifier _interfaceId, false otherwise | ||
* @dev Interface identification is specified in ERC-165. | ||
*/ | ||
function supportsInterface(address _address, bytes4 _interfaceId) | ||
internal | ||
view | ||
returns (bool) | ||
{ | ||
// query support of both ERC165 as per the spec and support of _interfaceId | ||
return supportsERC165(_address) && | ||
supportsERC165Interface(_address, _interfaceId); | ||
} | ||
|
||
/** | ||
* @notice Query if a contract implements interfaces, also checks support of ERC165 | ||
* @param _address The address of the contract to query for support of an interface | ||
* @param _interfaceIds A list of interface identifiers, as specified in ERC-165 | ||
* @return true if the contract at _address indicates support all interfaces in the | ||
* _interfaceIds list, false otherwise | ||
* @dev Interface identification is specified in ERC-165. | ||
*/ | ||
function supportsInterfaces(address _address, bytes4[] _interfaceIds) | ||
internal | ||
view | ||
returns (bool) | ||
{ | ||
// query support of ERC165 itself | ||
if (!supportsERC165(_address)) { | ||
return false; | ||
} | ||
|
||
// query support of each interface in _interfaceIds | ||
for (uint256 i = 0; i < _interfaceIds.length; i++) { | ||
if (!supportsERC165Interface(_address, _interfaceIds[i])) { | ||
return false; | ||
} | ||
} | ||
|
||
// all interfaces supported | ||
return true; | ||
} | ||
|
||
/** | ||
* @notice Query if a contract implements an interface, does not check ERC165 support | ||
* @param _address The address of the contract to query for support of an interface | ||
* @param _interfaceId The interface identifier, as specified in ERC-165 | ||
* @return true if the contract at _address indicates support of the interface with | ||
* identifier _interfaceId, false otherwise | ||
* @dev Assumes that _address contains a contract that supports ERC165, otherwise | ||
* the behavior of this method is undefined. This precondition can be checked | ||
* with the `supportsERC165` method in this library. | ||
* Interface identification is specified in ERC-165. | ||
*/ | ||
function supportsERC165Interface(address _address, bytes4 _interfaceId) | ||
private | ||
view | ||
returns (bool) | ||
{ | ||
// success determines whether the staticcall succeeded and result determines | ||
// whether the contract at _address indicates support of _interfaceId | ||
(bool success, bool result) = callERC165SupportsInterface( | ||
_address, _interfaceId); | ||
|
||
return (success && result); | ||
} | ||
|
||
/** | ||
* @notice Calls the function with selector 0x01ffc9a7 (ERC165) and suppresses throw | ||
* @param _address The address of the contract to query for support of an interface | ||
* @param _interfaceId The interface identifier, as specified in ERC-165 | ||
* @return success true if the STATICCALL succeeded, false otherwise | ||
* @return result true if the STATICCALL succeeded and the contract at _address | ||
* indicates support of the interface with identifier _interfaceId, false otherwise | ||
*/ | ||
function callERC165SupportsInterface( | ||
address _address, | ||
bytes4 _interfaceId | ||
) | ||
private | ||
view | ||
returns (bool success, bool result) | ||
{ | ||
bytes memory encodedParams = abi.encodeWithSelector( | ||
InterfaceId_ERC165, | ||
_interfaceId | ||
); | ||
|
||
// solium-disable-next-line security/no-inline-assembly | ||
assembly { | ||
let encodedParams_data := add(0x20, encodedParams) | ||
let encodedParams_size := mload(encodedParams) | ||
|
||
let output := mload(0x40) // Find empty storage location using "free memory pointer" | ||
mstore(output, 0x0) | ||
|
||
success := staticcall( | ||
30000, // 30k gas | ||
_address, // To addr | ||
encodedParams_data, | ||
encodedParams_size, | ||
output, | ||
0x20 // Outputs are 32 bytes long | ||
) | ||
|
||
result := mload(output) // Load the result | ||
} | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
pragma solidity ^0.4.24; | ||
|
||
import "../../introspection/ERC165.sol"; | ||
|
||
|
||
/** | ||
* https://github.com/ethereum/EIPs/blob/master/EIPS/eip-214.md#specification | ||
* > Any attempts to make state-changing operations inside an execution instance with STATIC set to true will instead throw an exception. | ||
* > These operations include [...], LOG0, LOG1, LOG2, [...] | ||
* | ||
* therefore, because this contract is staticcall'd we need to not emit events (which is how solidity-coverage works) | ||
* solidity-coverage ignores the /mocks folder, so we duplicate its implementation here to avoid instrumenting it | ||
*/ | ||
contract SupportsInterfaceWithLookupMock is ERC165 { | ||
|
||
bytes4 public constant InterfaceId_ERC165 = 0x01ffc9a7; | ||
/** | ||
* 0x01ffc9a7 === | ||
* bytes4(keccak256('supportsInterface(bytes4)')) | ||
*/ | ||
|
||
/** | ||
* @dev a mapping of interface id to whether or not it's supported | ||
*/ | ||
mapping(bytes4 => bool) internal supportedInterfaces; | ||
|
||
/** | ||
* @dev A contract implementing SupportsInterfaceWithLookup | ||
* implement ERC165 itself | ||
*/ | ||
constructor() | ||
public | ||
{ | ||
_registerInterface(InterfaceId_ERC165); | ||
} | ||
|
||
/** | ||
* @dev implement supportsInterface(bytes4) using a lookup table | ||
*/ | ||
function supportsInterface(bytes4 _interfaceId) | ||
external | ||
view | ||
returns (bool) | ||
{ | ||
return supportedInterfaces[_interfaceId]; | ||
} | ||
|
||
/** | ||
* @dev private method for registering an interface | ||
*/ | ||
function _registerInterface(bytes4 _interfaceId) | ||
internal | ||
{ | ||
require(_interfaceId != 0xffffffff); | ||
supportedInterfaces[_interfaceId] = true; | ||
} | ||
} | ||
|
||
|
||
|
||
contract ERC165InterfacesSupported is SupportsInterfaceWithLookupMock { | ||
constructor (bytes4[] _interfaceIds) | ||
public | ||
{ | ||
for (uint256 i = 0; i < _interfaceIds.length; i++) { | ||
_registerInterface(_interfaceIds[i]); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
pragma solidity ^0.4.24; | ||
|
||
|
||
contract ERC165NotSupported { | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
pragma solidity ^0.4.24; | ||
|
||
import "../introspection/ERC165Checker.sol"; | ||
|
||
|
||
contract ERC165CheckerMock { | ||
using ERC165Checker for address; | ||
|
||
function supportsERC165(address _address) | ||
public | ||
view | ||
returns (bool) | ||
{ | ||
return _address.supportsERC165(); | ||
} | ||
|
||
function supportsInterface(address _address, bytes4 _interfaceId) | ||
public | ||
view | ||
returns (bool) | ||
{ | ||
return _address.supportsInterface(_interfaceId); | ||
} | ||
|
||
function supportsInterfaces(address _address, bytes4[] _interfaceIds) | ||
public | ||
view | ||
returns (bool) | ||
{ | ||
return _address.supportsInterfaces(_interfaceIds); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
const ERC165CheckerMock = artifacts.require('ERC165CheckerMock'); | ||
const ERC165NotSupported = artifacts.require('ERC165NotSupported'); | ||
const ERC165InterfacesSupported = artifacts.require('ERC165InterfacesSupported'); | ||
|
||
const DUMMY_ID = '0xdeadbeef'; | ||
const DUMMY_ID_2 = '0xcafebabe'; | ||
const DUMMY_ID_3 = '0xdecafbad'; | ||
const DUMMY_UNSUPPORTED_ID = '0xbaddcafe'; | ||
const DUMMY_UNSUPPORTED_ID_2 = '0xbaadcafe'; | ||
const DUMMY_ACCOUNT = '0x1111111111111111111111111111111111111111'; | ||
|
||
require('chai') | ||
.should(); | ||
|
||
contract('ERC165Checker', function () { | ||
beforeEach(async function () { | ||
this.mock = await ERC165CheckerMock.new(); | ||
}); | ||
|
||
context('ERC165 not supported', function () { | ||
beforeEach(async function () { | ||
this.target = await ERC165NotSupported.new(); | ||
}); | ||
|
||
it('does not support ERC165', async function () { | ||
const supported = await this.mock.supportsERC165(this.target.address); | ||
supported.should.equal(false); | ||
}); | ||
|
||
it('does not support mock interface via supportsInterface', async function () { | ||
const supported = await this.mock.supportsInterface(this.target.address, DUMMY_ID); | ||
supported.should.equal(false); | ||
}); | ||
|
||
it('does not support mock interface via supportsInterfaces', async function () { | ||
const supported = await this.mock.supportsInterfaces(this.target.address, [DUMMY_ID]); | ||
supported.should.equal(false); | ||
}); | ||
}); | ||
|
||
context('ERC165 supported', function () { | ||
beforeEach(async function () { | ||
this.target = await ERC165InterfacesSupported.new([]); | ||
}); | ||
|
||
it('supports ERC165', async function () { | ||
const supported = await this.mock.supportsERC165(this.target.address); | ||
supported.should.equal(true); | ||
}); | ||
|
||
it('does not support mock interface via supportsInterface', async function () { | ||
const supported = await this.mock.supportsInterface(this.target.address, DUMMY_ID); | ||
supported.should.equal(false); | ||
}); | ||
|
||
it('does not support mock interface via supportsInterfaces', async function () { | ||
const supported = await this.mock.supportsInterfaces(this.target.address, [DUMMY_ID]); | ||
supported.should.equal(false); | ||
}); | ||
}); | ||
|
||
context('ERC165 and single interface supported', function () { | ||
beforeEach(async function () { | ||
this.target = await ERC165InterfacesSupported.new([DUMMY_ID]); | ||
}); | ||
|
||
it('supports ERC165', async function () { | ||
const supported = await this.mock.supportsERC165(this.target.address); | ||
supported.should.equal(true); | ||
}); | ||
|
||
it('supports mock interface via supportsInterface', async function () { | ||
const supported = await this.mock.supportsInterface(this.target.address, DUMMY_ID); | ||
supported.should.equal(true); | ||
}); | ||
|
||
it('supports mock interface via supportsInterfaces', async function () { | ||
const supported = await this.mock.supportsInterfaces(this.target.address, [DUMMY_ID]); | ||
supported.should.equal(true); | ||
}); | ||
}); | ||
|
||
context('ERC165 and many interfaces supported', function () { | ||
beforeEach(async function () { | ||
this.supportedInterfaces = [DUMMY_ID, DUMMY_ID_2, DUMMY_ID_3]; | ||
this.target = await ERC165InterfacesSupported.new(this.supportedInterfaces); | ||
}); | ||
|
||
it('supports ERC165', async function () { | ||
const supported = await this.mock.supportsERC165(this.target.address); | ||
supported.should.equal(true); | ||
}); | ||
|
||
it('supports each interfaceId via supportsInterface', async function () { | ||
for (const interfaceId of this.supportedInterfaces) { | ||
const supported = await this.mock.supportsInterface(this.target.address, interfaceId); | ||
supported.should.equal(true); | ||
}; | ||
}); | ||
|
||
it('supports all interfaceIds via supportsInterfaces', async function () { | ||
const supported = await this.mock.supportsInterfaces(this.target.address, this.supportedInterfaces); | ||
supported.should.equal(true); | ||
}); | ||
|
||
it('supports none of the interfaces queried via supportsInterfaces', async function () { | ||
const interfaceIdsToTest = [DUMMY_UNSUPPORTED_ID, DUMMY_UNSUPPORTED_ID_2]; | ||
|
||
const supported = await this.mock.supportsInterfaces(this.target.address, interfaceIdsToTest); | ||
supported.should.equal(false); | ||
}); | ||
|
||
it('supports not all of the interfaces queried via supportsInterfaces', async function () { | ||
const interfaceIdsToTest = [...this.supportedInterfaces, DUMMY_UNSUPPORTED_ID]; | ||
|
||
const supported = await this.mock.supportsInterfaces(this.target.address, interfaceIdsToTest); | ||
supported.should.equal(false); | ||
}); | ||
}); | ||
|
||
context('account address does not support ERC165', function () { | ||
it('does not support ERC165', async function () { | ||
const supported = await this.mock.supportsERC165(DUMMY_ACCOUNT); | ||
supported.should.equal(false); | ||
}); | ||
|
||
it('does not support mock interface via supportsInterface', async function () { | ||
const supported = await this.mock.supportsInterface(DUMMY_ACCOUNT, DUMMY_ID); | ||
supported.should.equal(false); | ||
}); | ||
|
||
it('does not support mock interface via supportsInterfaces', async function () { | ||
const supported = await this.mock.supportsInterfaces(DUMMY_ACCOUNT, [DUMMY_ID]); | ||
supported.should.equal(false); | ||
}); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aren't these tests repeats of
context('ERC165 not supported')
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but specifically for EOA accounts, not a contract deployed to the chain
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, I guess those are different enough to make this a meaningful test, thanks!