ERC Interface & Extension Standards (EIP-165, EIP-173, ERC-3156)
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;
}
OpenZeppelin Ownable2Step (Recommended)
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",
};