The forgotten ERCs & EIPs: ERC165

Alejo Lovallo
Think and Dev
Published in
4 min readMar 22, 2023

Purpose of this document and the following ones:

We all know a couple of ERCs and we use them almost every day while we are developing blockchain projects:

  • ERC20
  • ERC721
  • ERC1155

Great! We could all agree that we know how they work and how to use them.

However, we tend to forget we have another bunch of standards that are being used productively behind the curtains for many common ERCs. They are as important as any of the famous ones.

Today we will know in detail ERC-165

Function selectors

To talk about ERC-165, we must know what function selectors are first.

From official Solidity documentation:

When a function is called, the first 4 bytes of calldata specifies which function to call.

This 4 bytes is called a function selector.

So it is nothing more than the signature of a function inside a smart contract. Keep that in mind.

Interfaces

Interfaces are a common means to interact with other contracts. They define what a contract can do through its functions definitions.

So, if we have for instance a contract A with the following code:

contract A {
address public owner;

constructor(address _owner) {
owner = _owner;
}

function changeOwner(address _newOwner) external {
owner = _newOwner;
]

}

The interface for contract A will look like the following:

interface IA {
function changeOwner(address _newOwner) external;
}

Lastly, if contract B wants to use contract A, it could make use of IA like this:

import "IA.sol";

contract B {
function changeOwnerA (address AContract,address _newOwner) external {
IA(AContract).changeOwner(_newOner);
}
}

Excellent!, Well…. We might have several problems here:

  • We have no way of being sure that the address AContract is either a smart contract or a simple address.
  • If it is a smart contract, we have no true way of validating that it has implemented the changeOwner function.

The first problem has been already solved by checking inside the smart contract if an address has its extcodesize different from zero and thus, it is not an EOA.

However, for the last problem, we are not so lucky. That´s exactly what ERC-165 solved.

ERC-165 to the rescue

The standard aims to create a standard method to publish and detect what interfaces a smart contract implements.

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

And that´s it. Only one interface and one function to define in our contract to avoid this particular problem.

Let´s see now how to implement it for our contract A.


import "./IA.sol";
import “./ERC165/ERC165.sol”;

contract A is ERC165, IA{
address public owner;

constructor(address _owner) {
owner = _owner;
}

function changeOwner(address _newOwner) external {
owner = _newOwner;
]

function supportsInterface(bytes4 interfaceId) external view returns (bool) {
return interfaceId == 0x01ffc9a7 || //ERC165 InterfaceID
interfaceId == 0x79c2449; // Contract A InterfaceID (made up value)
}
}

At this point, you will probably be a bit lost. For the first part of the function:

interfaceId == 0x01ffc9a7

This comes from the official ERC documentation:

The supportInterface function will return true when interfaceID is 0x01ffc9a7 (EIP165 interface)

So far so good, but what is interfaceID?

ERC165 defines that an interface ID can be calculated as the XOR of all function selectors in the interface.

To get the actual interface ID for our A contract, we must create a helper contract:

import "./A.sol";

contract GetContractInterfaceId {

function calcStoreInterfaceId() external pure returns (bytes4) {
A aContract;
return aContract.changeOwner.selector;
}
}

Excellent, so finally, to solve the initial problem we could refactor our contract B code knowing the contract A interface ID:

import "IA.sol";
import "./ERC165/ERC165.sol";

contract B {
function changeOwnerA (address AContract,address _newOwner) external {
require(ERC165(AContract).supportsInterface(0x79c2449),"Invalid AContract");
IA(AContract).changeOwner(_newOner);
}
}

Now we could be 100% percent sure that our contract will only allow a valid A smart contract.

That´s all for today. However, never forget….

There is always more to come

--

--

Alejo Lovallo
Think and Dev

Sr. Blockchain Developer || WIP Software engineer || DevOps Consultant.