standards/erc-interface-standards

ERC Interface & Extension Standards (EIP-165, EIP-173, ERC-3156)

ethereumstandard-guide🏛️ Officialconfidence highhealth 100%
v1.0.0·Updated 3/26/2026

Overview

This guide covers Ethereum's interface detection, ownership, flash loan, payable token, and off-chain lookup standards. These are utility standards that make contracts interoperable and composable.

EIP-165 — Standard Interface Detection

EIP-165 allows callers to query whether a contract implements a specific interface, identified by a 4-byte interface ID.

interface IERC165 {
    // Returns true if contract implements the interface
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

Computing Interface IDs

An interface ID is the XOR of all function selectors in the interface:

// Compute at compile time
bytes4 constant ERC721_INTERFACE_ID = type(IERC721).interfaceId;
// = transferFrom.selector ^ safeTransferFrom.selector ^ ... (XOR of all selectors)

// Common interface IDs
bytes4 constant ERC165_ID    = 0x01ffc9a7;
bytes4 constant ERC721_ID    = 0x80ac58cd;
bytes4 constant ERC721Meta_ID = 0x5b5e139f;
bytes4 constant ERC1155_ID   = 0xd9b67a26;
bytes4 constant ERC2981_ID   = 0x2a55205a;
bytes4 constant ERC4907_ID   = 0xad092b5c;

Implementation

import "@openzeppelin/contracts/utils/introspection/ERC165.sol";

contract MyToken is ERC165, IERC721 {
    function supportsInterface(bytes4 interfaceId)
        public view override returns (bool)
    {
        return
            interfaceId == type(IERC721).interfaceId ||
            interfaceId == type(IERC721Metadata).interfaceId ||
            super.supportsInterface(interfaceId); // Handles ERC165 itself
    }
}

Querying Interface Support (ethers.js)

import { ethers } from "ethers";

async function supportsInterface(
  contractAddress: string,
  interfaceId: string, // e.g., "0x80ac58cd"
  provider: ethers.Provider
): Promise<boolean> {
  const contract = new ethers.Contract(
    contractAddress,
    ["function supportsInterface(bytes4) view returns (bool)"],
    provider
  );
  try {
    return await contract.supportsInterface(interfaceId);
  } catch {
    return false; // Contract doesn't implement ERC-165
  }
}

// Example: check if contract is ERC-721
const isNFT = await supportsInterface(address, "0x80ac58cd", provider);

Never assume a contract supports an interface without checking — always use EIP-165 for robust integrations.

EIP-173 — Contract Ownership Standard

EIP-173 standardizes contract ownership with a owner() getter and transferOwnership() function.

interface IERC173 {
    event OwnershipTransferred(
        address indexed previousOwner,
        address indexed newOwner
    );

    function owner() external view returns (address);

    function transferOwnership(address newOwner) external;
}

Always use two-step ownership transfer to prevent accidentally setting owner to wrong address:

import "@openzeppelin/contracts/access/Ownable2Step.sol";

contract MyContract is Ownable2Step {
    constructor() Ownable(msg.sender) {}

    function adminFunction() external onlyOwner {
        // Only owner can call this
    }
}

// Transfer: owner calls transferOwnership(newOwner)
// Then: newOwner must call acceptOwnership()
// This prevents accidental loss of ownership

Renouncing Ownership

// Permanently removes ownership — use with extreme caution
contract.renounceOwnership(); // Sets owner to address(0)

Common pattern: Renounce ownership after deploying to make a contract truly immutable and trustless.

ERC-1820 — Pseudo-introspection Registry

ERC-1820 provides a global registry where contracts and addresses can register which interfaces they implement.

interface IERC1820Registry {
    function setInterfaceImplementer(
        address account,
        bytes32 interfaceHash,
        address implementer
    ) external;

    function getInterfaceImplementer(
        address account,
        bytes32 interfaceHash
    ) external view returns (address);

    function interfaceHash(string calldata interfaceName)
        external pure returns (bytes32);
}

// Singleton at same address on all chains:
// 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24

When to use: ERC-777 token hooks rely on ERC-1820 (legacy). For new contracts, prefer EIP-165 directly.

ERC-3156 — Flash Loans

ERC-3156 standardizes flash loan interfaces for both lenders and borrowers.

interface IERC3156FlashLender {
    // Maximum flash loan amount for a token
    function maxFlashLoan(address token) external view returns (uint256);

    // Fee for flash loan (in token units)
    function flashFee(address token, uint256 amount) external view returns (uint256);

    // Initiate flash loan
    function flashLoan(
        IERC3156FlashBorrower receiver,
        address token,
        uint256 amount,
        bytes calldata data
    ) external returns (bool);
}

interface IERC3156FlashBorrower {
    // Called by lender during flash loan
    function onFlashLoan(
        address initiator,   // Who initiated the loan
        address token,       // Token borrowed
        uint256 amount,      // Amount borrowed
        uint256 fee,         // Fee to repay
        bytes calldata data  // Arbitrary data
    ) external returns (bytes32); // Must return keccak256("ERC3156FlashBorrower.onFlashLoan")
}

Flash Loan Borrower Implementation

import "@openzeppelin/contracts/interfaces/IERC3156FlashBorrower.sol";

contract MyArbitrageur is IERC3156FlashBorrower {
    bytes32 private constant CALLBACK_SUCCESS =
        keccak256("ERC3156FlashBorrower.onFlashLoan");

    IERC3156FlashLender public immutable lender;

    constructor(address _lender) {
        lender = IERC3156FlashLender(_lender);
    }

    function executeArbitrage(address token, uint256 amount) external {
        // 1. Initiate flash loan
        lender.flashLoan(this, token, amount, abi.encode(/* params */));
    }

    function onFlashLoan(
        address initiator,
        address token,
        uint256 amount,
        uint256 fee,
        bytes calldata data
    ) external override returns (bytes32) {
        require(msg.sender == address(lender), "Untrusted lender");
        require(initiator == address(this), "Untrusted initiator");

        // 2. Use the funds (arbitrage, liquidation, etc.)
        // ... your logic here ...

        // 3. Approve repayment (amount + fee)
        IERC20(token).approve(address(lender), amount + fee);

        return CALLBACK_SUCCESS;
    }
}

Flash Loan Security

// Always validate both msg.sender and initiator
require(msg.sender == address(lender), "Untrusted lender");
require(initiator == address(this), "External call not allowed");

// Reentrancy protection
modifier nonReentrant() { ... }

// Verify repayment in lender contract
require(
    IERC20(token).balanceOf(address(this)) >= balanceBefore + fee,
    "Flash loan not repaid"
);

ERC-1363 — Payable Token

ERC-1363 extends ERC-20 to allow contracts to receive tokens and be notified in a single transaction (like ETH receive() for tokens).

interface IERC1363 is IERC20 {
    // Transfer tokens and call receiver's onTransferReceived
    function transferAndCall(address to, uint256 amount) external returns (bool);
    function transferAndCall(address to, uint256 amount, bytes calldata data) external returns (bool);

    // TransferFrom and call receiver
    function transferFromAndCall(address from, address to, uint256 amount) external returns (bool);
    function transferFromAndCall(address from, address to, uint256 amount, bytes calldata data) external returns (bool);

    // Approve and call spender's onApprovalReceived
    function approveAndCall(address spender, uint256 amount) external returns (bool);
    function approveAndCall(address spender, uint256 amount, bytes calldata data) external returns (bool);
}

interface IERC1363Receiver {
    function onTransferReceived(
        address operator,
        address from,
        uint256 amount,
        bytes calldata data
    ) external returns (bytes4); // Must return 0x88a7ca5c
}

Use case: Token-powered subscriptions, single-step token deposits to contracts, token-gated access.

ERC-3668 — CCIP-Read (Off-Chain Lookups)

ERC-3668 (Cross-Chain Interoperability Protocol) standardizes off-chain data lookups for contracts. Enables ENS wildcard resolution and off-chain data without trusting a centralized gateway.

// Contract signals off-chain lookup needed by reverting with:
error OffchainLookup(
    address sender,
    string[] urls,      // HTTPS gateway URLs to query
    bytes callData,     // Data to send to gateway
    bytes4 callbackFunction, // Function to call with response
    bytes extraData     // Forwarded to callback
);

// Callback function receives gateway response
function resolveWithProof(
    bytes calldata response,
    bytes calldata extraData
) external view returns (bytes memory);

Flow: Client calls contract → Contract reverts with OffchainLookup → Client queries gateway → Client calls callback with proof → Contract verifies and returns data.

Use cases: ENS L2 resolution, off-chain NFT metadata verification, cross-chain data attestations.

Interface IDs Quick Reference

const INTERFACE_IDS = {
  ERC165:         "0x01ffc9a7",
  ERC173:         "0x7f5828d0", // ownerOf + transferOwnership
  ERC721:         "0x80ac58cd",
  ERC721Metadata: "0x5b5e139f",
  ERC721Enumerable: "0x780e9d63",
  ERC1155:        "0xd9b67a26",
  ERC1155Meta:    "0x0e89341c",
  ERC2981:        "0x2a55205a",
  ERC4906:        "0x49064906",
  ERC4907:        "0xad092b5c",
  ERC5192:        "0xb45a3c0e",
};