Эх сурвалжийг харах

Add ERC165Query library (#1086)

* Add ERC165Query library

* Address PR Comments

* Add tests and mocks from #1024 and refactor code slightly

* Fix javascript and solidity linting errors

* Split supportsInterface into three methods as discussed in #1086

* Change InterfaceId_ERC165 comment to match style in the rest of the repo

* Fix max-len lint issue on ERC165Checker.sol

* Conditionally ignore the asserts during solidity-coverage test

* Switch to abi.encodeWithSelector and add test for account addresses

* Switch to supportsInterfaces API as suggested by @frangio

* Adding ERC165InterfacesSupported.sol

* Fix style issues

* Add test for supportsInterfaces returning false

* Add ERC165Checker.sol newline

* feat: fix coverage implementation

* fix: solidity linting error

* fix: revert to using boolean tests instead of require statements

* fix: make supportsERC165Interface private again

* rename SupportsInterfaceWithLookupMock to avoid name clashing
Lev Dubinets 7 жил өмнө
parent
commit
2adb491637

+ 149 - 0
contracts/introspection/ERC165Checker.sol

@@ -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 - 0
contracts/mocks/ERC165/ERC165InterfacesSupported.sol

@@ -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 - 0
contracts/mocks/ERC165/ERC165NotSupported.sol

@@ -0,0 +1,6 @@
+pragma solidity ^0.4.24;
+
+
+contract ERC165NotSupported {
+
+}

+ 32 - 0
contracts/mocks/ERC165CheckerMock.sol

@@ -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 - 0
test/introspection/ERC165Checker.test.js

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