ERC165

Our first open source contribuition

Guihcneves
Blockful
Published in
4 min readAug 2, 2022

--

ERC-165 is the standard for interface management. It is incredibly used by many known protocols.

While executing tests and implementing a feature in ENS contracts, we saw an opportunity to improve testing with an open source contribution.

Before finding that the test errors were related to interface id

An example of how the erc165 library can be used to calculate the interface id from the ABI of a given contract and explain how this standard works.

EIP-165

The ERC-165 is the approved EIP-165, representing the Interface implementation as a standard. It creates a method to publish and detect what interface is being implemented.

Calldata is an immutable, temporary location where function arguments are stored, and behaves mostly like memory. Every function called will have the first 4 bytes of the calldata specifying which function to call. These first 4 bytes are called a function selector and are a Keccak-256 hash of the signature of the function.

signature = name()

selector = 0x06fdde03

There are multiple ways of finding the selector:

bytes4(keccak256(‘name()’))

interface.function.selector

bytes4(sha3(‘name()’))

The output will always be a bytes4 as “0x06fdde03”.

The interface id is the XOR of all function selectors in the interface.

Applicability

The contract Lisa is an ERC165MappingImplementation which has the IERC165, allowing us to check for interface availability. Mapping will be used to guarantee the relationship between keys and values as interface id and booleans. Whenever a function from a contract wants to be called, the caller must implement the contract’s interface to match the correct selector.

The mapping function is generic and reusable. In the long run, gas will be saved as multiple contracts share the same interface.

Lisa contract

When both ERC165MappingImplementation and Lisa are deployed, the supportedInterface will both add their respectful interface ids into the mapping, setting their booleans to true.

We calculate the interface id by calling the selector of the function, or calling the interface id of that interface type ( Interface must be implemented ):

return ILisa.is2D.selector ^ ILisa.skinColor.selector;

return this.is2D.selector ^ this.skinColor.selector;

return type(ILisa).interfaceId;

All three methods will result in the same output: 0x73b6b492.

Comparison

The use of interfaces is an important practice due to the reuse of on-chain code. We ran a gas usage experiment in the Lisa and the Homer contract.

The Homer contract is a cheaper way to first deploy the interface method. Although it does not store a mapping of interfaces, meaning it will need to be deployed for every next interface instance.

Homer contract

The Lisa method is more elegant due to its portability and versioning control. In order to adapt to the way the contract is being interacted with, the interface must match the criteria.

At deployment on Remix, the:

Lisa used 280450 wei.

Homer used 228584 wei.

The amount of gas for every new interface id implemented using Lisa is 43925. While every instance using Homer will be around 200000. With three or more supported interfaces (including ERC-165 itself as a required supported interface), the mapping approach (in every case) costs less gas than the pure approach (in the worst case).

Implementation

Accessing the selector function on-chain is very useful and can provide incredible interoperability for an ecosystem, although it is a very known issue, that the interface id is sometimes needed off-chain for comparisons and tests purposes.

Here comes in handy an integration, ERC-165 is a library that uses the compiled ABI of a given contract to externally calculate the interface id using keccak256 and XOR functions.

Using the map function to get each parameter type inside the function from the ABI and return the signature.

const prepareData = e => `${e.name}(${e.inputs.map(e => e.type)})`

Calculate the selector, by hashing the signature, using keccak256, and slicing the first 8 characters representing the 4bytes interface id size. Finished by adding the 0x to match hexadecimal criteria.

const encodeSelector = f => “0x” + keccak256(f).slice(0, 8);

Creating an array of selectors contained in the given contract.

const functionSelectors = abi    .filter(e => e.type === “function”)    .flatMap(e => `${encodeSelector(prepareData(e))}`);

Using reduce to XOR all the selectors, converting the result into the hexadecimal in string base16 and adding the 0x in front to match solidity criteria.

const interfaceId = “0x” + functionSelectors.reduce((prev, cur) => prev ^ cur).toString(16);

To start working with erc165 you must:

Install the npm package inside your environment.

npm i erc165

Require it at the beginning of the file.

// CommonJS
const erc165 = require(“erc165”);
//ES6
import * as erc165 from "erc165";

Demo

To easily understand how it works, we prepared a simple demo using a hardhat test environment. Get the full repository here and follow these steps:

Download the repository.

git clone https://github.com/blockful-io/erc165-example.git

Install Dependencies.

npm i

Run tests.

npx hardhat test

Output:

Output test for Lisa and Homer contracts.

Conclusion

We’ve seen how to set the standard interface method IERC165 and implement a mapping to store the interface implementations and best practices to achieve better quality outputs. To save gas and avoid turnarounds to get the interface id of contracts, the npm library erc165 has shown to be a very elegant way of minimizing the trouble of calculating function selectors and automating more testing.

If you find any bugs or have any implementations please do submit a pull request.

Blockful | 🐦 | 🐱‍👤

--

--