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

Add ERC165Query library #1086

Merged
merged 20 commits into from
Aug 22, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions contracts/introspection/ERC165Checker.sol
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
}
}
}

69 changes: 69 additions & 0 deletions contracts/mocks/ERC165/ERC165InterfacesSupported.sol
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]);
}
}
}
6 changes: 6 additions & 0 deletions contracts/mocks/ERC165/ERC165NotSupported.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pragma solidity ^0.4.24;


contract ERC165NotSupported {

}
32 changes: 32 additions & 0 deletions contracts/mocks/ERC165CheckerMock.sol
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);
}
}
137 changes: 137 additions & 0 deletions test/introspection/ERC165Checker.test.js
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 () {
Copy link
Contributor

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')?

Copy link
Contributor

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

Copy link
Contributor

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!

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