# ERC-165: Interface Detection and Token Receivers

Author: Markus Waas

Published: 2020-10-25T05:46:13.000Z

Updated: 2026-09-13T14:25:30.000Z

Source: [https://soliditydeveloper.com/eip-165](<https://soliditydeveloper.com/eip-165>)

## Compatibility and review

Before you start

The examples target Solidity 0.7.4 and OpenZeppelin Contracts 3. ERC-165 is a self-reported interface declaration, not proof of correct or safe behavior. ERC-20 does not require it, and ordinary token transfers do not automatically consult it.

[Official reference](<https://eips.ethereum.org/EIPS/eip-165>)

![Dark Forest](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/01795919c512c226/e5e9d122c0da/v/830e15e79ec0/darkforest.jpeg>)

Do you remember the beginning of the [Dark Forest](<https://medium.com/@danrobinson/ethereum-is-a-dark-forest-ecc5f0505dff>)story? If not, let's look at it again:

> On Wednesday afternoon, someone asked whether it was possible to recover Uniswap liquidity tokens that had been accidentally sent to the pair contract itself.
>
>
>
> Dan Robinson

Somebody sent tokens to a smart contract that was not intended to receive tokens. This perfectly illustrates one of the issues not only with ERC-20 tokens, but generally with smart contracts. ***How can we find out if a contract actually supports being the receiver or owner of some interface/token?***

You can send tokens to any smart contract, but they will mostly just be locked and not usable. This is because a smart contract (in contrast to an [EOA](<https://ethereum.stackexchange.com/a/5829/33305>)) is not able to do arbitrary calls to other smart contracts. It only supports the functionality that actually has been implemented.

This has resulted in a lot of tokens being lost. Just take a look at following token contracts. Those are all ERC-20 contracts where users sent the token directly to the contract itself, forever locking them:

- **GNT**, $35,000 lost ([see on Etherscan](<https://etherscan.io/address/0xa74476443119A942dE498590Fe1f2454d7D4aC0d>))
- **DGD**, $62,000 lost ([see on Etherscan](<https://etherscan.io/address/0xe0b7927c4af23765cb51314a0e0521a9645f0e2a>))
- **OMG**, $82,000 lost ([see on Etherscan](<https://etherscan.io/address/0xd26114cd6ee289accf82350c8d8487fedb8a0c07>))
- **ZRX**, $92,000 lost ([see on Etherscan](<https://etherscan.io/address/0xe41d2489571d322189246dafa5ebde1f4699f498>))

This is where [EIP-165](<https://eips.ethereum.org/EIPS/eip-165>) comes in. Let's take a closer look at it.

## What is EIP-165?

At its core it's actually just one function:

```solidity
interface ERC165 {
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
```

Now a contract can implement this interface and return true for every supported `interfaceID`.

**What is an interfaceID?**

The given interface ID is an identifier that is computed as the XOR (exclusive OR) of all [function selectors](<https://solidity.readthedocs.io/en/v0.7.4/abi-spec.html#function-selector>) that are part of the interface.

**How do you calculate the id?**

Previously you had to do a calculation as shown on the right. This would calculate the required XOR from all ERC20 functions. XOR is independent of selector order, but it does not make each selector change every output bit. The four-byte result identifies this selector set; it is not a unique cryptographic fingerprint or proof of correct behavior.

Since Solidity 0.7.2 you can now write `type(IERC20).interfaceId`.

```solidity
function calcErc20InterfaceId() returns (bytes4) {
        return ERC20.transfer.selector
            ^ ERC20.transferFrom.selector
            ^ ERC20.approve.selector
            ^ ERC20.allowance.selector
            ^ ERC20.totalSupply.selector
            ^ ERC20.balanceOf.selector;
    }
```

A function selector is four bytes, and ERC-165 combines selectors into a four-byte identifier. There are 2^32 possible bit patterns; `0xffffffff` is reserved and must return false. A mapping keyed by bytes4 is convenient, but its separate entries each occupy their own storage location rather than packing all interface flags together.

Collisions are still possible. Maybe you’ve heard of the birthday paradox? Let’s do an interesting short excursion...

![Birthday Raptor](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/b22a3beaa5992cc4/60919140cdf4/v/a7c2a1422b49/birthday-raptor.jpg>)

*No, we don't mean this paradox.*

The [birthday paradox](<https://en.wikipedia.org/wiki/Birthday_problem>) states you don't need to have a lot of people in the same room for two of them to have the same birthday, despite there being 365 days in a year. In fact, just 23 people in one room will have a 50% chance to have at least one match.

Transferred to our scenario, you can use a calculator: [https://instacalc.com/28845](<https://instacalc.com/28845>) to compute collisions for our interface IDs.

![Bday Paradox Interfaces 10k](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/663c0ed525b91558/781bad897d5c/v/819f37289242/bday-paradox-interfaces-10k.png>)

![Bday Paradox Interfaces 100k](<https://cdn0.scrvt.com/b095ee27d37b3d7b6b150adba9ac6ec8/28df6ad655f1f2b1/099f850dd901/v/475834ec1ab0/bday-paradox-interfaces-100k.png>)

Given 10,000 different interfaces, there is a 98.84% chance of no collisions, while 100,000 will yield only 31%. However this is likely still good enough, because to really be a problem not only needs there be a collision, but somebody actually has to use exactly the two colliding interfaces by accident interchangeably. Think of it as it's okay to have people with the same birthdays in the world, just not in the same room.

### Base EIP-165 Implementation

Now with this in mind, a base implementation could look as shown on the right. Any contract supporting EIP-165 must also return true for the interface of the supportsInterface function itself.

```solidity
contract ERC165Implementation is ERC165 {
    mapping(bytes4 => bool) private supportedInterfaces;

    constructor() {
        supportedInterfaces[this.supportsInterface.selector] = true;
    }

    function supportsInterface(bytes4 interfaceID) external view override returns (bool) {
        return interfaceID != 0xffffffff && supportedInterfaces[interfaceID];
    }
}
```

Like this the contract is not yet any useful. Let's see how you could use it today with the latest tools using:

- Solidity 0.7.2+
- v3.2 Openzeppelin contracts for Solidity 0.7
- Truffle, Buidler or Remix

## Example Usage for ERC-20 tokens

### 1a. Adding EIP-165 to your ERC-20

Let's see how we can use EIP-165 with ERC-20 tokens. Since [Solidity v0.7.2](<https://github.com/ethereum/solidity/releases/tag/v0.7.2>) there is built-in EIP-165 support.

1. Start a project with Truffle, Buidler or Remix, you can follow the instructions [here](<https://soliditydeveloper.com/erc-2020>) if you need to.
2. Install the [openzeppelin contracts](<https://openzeppelin.com/contracts/>). Since we require Solidity 0.7, we need the newest pre-release.
3. Create a `TestERC20` token contract as shown on the right.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.7.4;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/introspection/ERC165.sol";

contract TestERC20 is ERC20, ERC165 {
    constructor() ERC20("","") {
        _registerInterface(type(IERC20).interfaceId);
        _registerInterface(ERC20.name.selector);
        _registerInterface(ERC20.symbol.selector);
        _registerInterface(ERC20.decimals.selector);
    }
}
```

As you can see, we can

```solidity
// import in Remix
import "http://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.1-solc-0.7/contracts/token/ERC20/ERC20.sol";
import "http://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.1-solc-0.7/contracts/introspection/ERC165.sol";
import "http://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.2.1-solc-0.7/contracts/introspection/ERC165Checker.sol";
```

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.7.4;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/introspection/ERC165Checker.sol";

contract UsingTestERC20 {
    using ERC165Checker for address;
    
    function addToken(address test) external {
        require(
            test.supportsInterface(type(IERC20).interfaceId),
            'Address is not supported'
        );
        
        // store token now
    }
}
```

### 1b. Checking for implemented ERC-20 interfaces

This check asks whether an address *declares* the ERC-20 interface. Standard ERC-20 tokens do not have to implement ERC-165, so many valid tokens will return no supported result. A contract can also return true incorrectly or dishonestly; ERC-165 does not prove that future token calls will succeed or that the implementation is safe.

### 2a. Adding EIP-165 to a token storage contract

Now remember back the issue with money lost for tokens being sent to a non-supporting contract. Let's see how we can solve this problem.

Let's use a storage contract that contains a `withdrawToOwner` function. Given this withdraw function, we can send any ERC-20 tokens to this smart contract without the funds getting lost.

Once again, to use the EIP-165 we just register the interface in the constructor. And we implement a simple `withdrawToOwner` function.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.7.4;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/introspection/ERC165.sol";

interface ArbitraryTokenStorage {
    function withdrawToOwner(IERC20 token) external;
}

contract ERC20Storage is ERC165, ArbitraryTokenStorage {
    address public owner;

    constructor() {
        owner = msg.sender;
        _registerInterface(type(ArbitraryTokenStorage).interfaceId);
    }
    
    function withdrawToOwner(IERC20 token) external override {
        uint256 balance = token.balanceOf(address(this));
        
        require(balance > 0, "Contract has no balance");
        require(token.transfer(owner, balance), "Transfer failed");
    }
}
```

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.7.4;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/introspection/ERC165Checker.sol";

contract UsingTestERC20 {
    using ERC165Checker for address;
    
    function secureSendToken(IERC20 token, address to, uint256 amount) external {
        require(
            to.supportsInterface(type(ArbitraryTokenStorage).interfaceId),
            'Address is not supported'
        );

        require(token.transfer(to, amount), "Transfer failed");
    }
}
```

### 2b. Checking for implemented token storage interfaces

Now we implement a second contract with a `secureSendToken` function. We send tokens from our contract to some other contract, but only if it is in fact an `ArbitraryTokenStorage` contract.

If it's any other contract or not a contract, the function will revert. Ensuring we don't sent tokens to an invalid (not supporting) address.

This protection only applies when the sending code performs the check. Ordinary ERC-20 transfers do not call it automatically, and a positive declaration still does not guarantee a working withdrawal path. Verify the receiving contract’s behavior and your intended asset flow.

## What's next?

Now that you know EIP-165, consider using it in your contracts. It's not always necessary in my opinion. But in fact EIP-165 is already used in quite a few other standards including the known ERC-721 and [ERC-777](<https://eips.ethereum.org/EIPS/eip-777>).

Actually ERC-777 is using the newer [EIP-1820](<https://eips.ethereum.org/EIPS/eip-1820>) which has backwards compatibility to EIP-165, but adds additional functionality for non-contract addresses to register an interface. We will look at EIP-1820 and ERC-777 in more details in the future.

What's your take on EIP-165? Have you already used it?
