Skip to content

Interfaces

Interfaces define contracts without implementation, enabling interoperability.

Defining Interfaces

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

Interface Rules

Interfaces can contain:

  • Function signatures (no implementation)
  • Events
  • Errors
  • Struct and enum definitions

Interfaces cannot contain:

  • Function implementations
  • State variables
  • Constructors
  • Modifiers
interface IVault {
    // Events - allowed
    event Deposit(address indexed user, uint256 amount);
    event Withdrawal(address indexed user, uint256 amount);

    // Errors - allowed
    error InsufficientBalance(uint256 available, uint256 required);

    // Structs - allowed
    struct Position {
        uint256 amount;
        uint256 timestamp;
    }

    // Enums - allowed
    enum Status { Active, Paused, Closed }

    // Function signatures - allowed (no body)
    function deposit(uint256 amount) external;
    function withdraw(uint256 amount) external;
    function getPosition(address user) external view returns (Position memory);
}

Implementing Interfaces

contract Token is IERC20 {
    string public name;
    uint256 public override totalSupply;
    mapping(address => uint256) private balances;
    mapping(address => mapping(address => uint256)) private allowances;

    constructor(string memory _name, uint256 _supply) {
        name = _name;
        totalSupply = _supply;
        balances[msg.sender] = _supply;
    }

    function balanceOf(address account) external view override returns (uint256) {
        return balances[account];
    }

    function transfer(address to, uint256 amount) external override returns (bool) {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        balances[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }

    function allowance(address owner, address spender) external view override returns (uint256) {
        return allowances[owner][spender];
    }

    function approve(address spender, uint256 amount) external override returns (bool) {
        allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) external override returns (bool) {
        require(allowances[from][msg.sender] >= amount, "Not allowed");
        require(balances[from] >= amount, "Insufficient balance");

        allowances[from][msg.sender] -= amount;
        balances[from] -= amount;
        balances[to] += amount;

        emit Transfer(from, to, amount);
        return true;
    }
}

Using Interfaces

Calling External Contracts

contract Wallet {
    function sendTokens(
        IERC20 token,
        address to,
        uint256 amount
    ) public {
        bool success = token.transfer(to, amount);
        require(success, "Transfer failed");
    }

    function checkBalance(
        IERC20 token,
        address account
    ) public view returns (uint256) {
        return token.balanceOf(account);
    }
}

Interface as Parameter Type

contract Exchange {
    function swap(
        IERC20 tokenIn,
        IERC20 tokenOut,
        uint256 amountIn
    ) public returns (uint256 amountOut) {
        // Transfer tokens in
        tokenIn.transferFrom(msg.sender, address(this), amountIn);

        // Calculate output
        amountOut = calculateOutput(amountIn);

        // Transfer tokens out
        tokenOut.transfer(msg.sender, amountOut);
    }
}

Interface as Return Type

contract Factory {
    mapping(uint256 => IERC20) public tokens;

    function getToken(uint256 id) external view returns (IERC20) {
        return tokens[id];
    }
}

Interface Inheritance

interface IERC20Basic {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
}

interface IERC20 is IERC20Basic {
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

interface IERC20Metadata is IERC20 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
}

Multiple Interface Implementation

interface IOwnable {
    function owner() external view returns (address);
    function transferOwnership(address newOwner) external;
}

interface IPausable {
    function paused() external view returns (bool);
    function pause() external;
    function unpause() external;
}

contract Token is IERC20, IOwnable, IPausable {
    // Implement all interface functions
    address public override owner;
    bool public override paused;

    function transferOwnership(address newOwner) external override {
        require(msg.sender == owner, "Not owner");
        owner = newOwner;
    }

    function pause() external override {
        require(msg.sender == owner, "Not owner");
        paused = true;
    }

    function unpause() external override {
        require(msg.sender == owner, "Not owner");
        paused = false;
    }

    // ... IERC20 implementations
}

Common Interface Patterns

Callback Interface

interface ICallback {
    function onTokenReceived(
        address sender,
        uint256 amount,
        bytes calldata data
    ) external returns (bytes4);
}

contract Token {
    function safeTransfer(
        address to,
        uint256 amount,
        bytes memory data
    ) public {
        transfer(to, amount);

        // Check if recipient is a contract
        if (isContract(to)) {
            bytes4 response = ICallback(to).onTokenReceived(
                msg.sender,
                amount,
                data
            );
            require(
                response == ICallback.onTokenReceived.selector,
                "Invalid callback"
            );
        }
    }
}

Factory Interface

interface IFactory {
    event Created(address indexed instance);

    function create(bytes calldata params) external returns (address);
    function getInstance(uint256 id) external view returns (address);
    function getInstanceCount() external view returns (uint256);
}

Registry Interface

interface IRegistry {
    function register(address instance) external;
    function unregister(address instance) external;
    function isRegistered(address instance) external view returns (bool);
    function getAll() external view returns (address[] memory);
}

Oracle Interface

interface IPriceOracle {
    function getPrice(address token) external view returns (uint256);
    function updatePrice(address token, uint256 price) external;

    event PriceUpdated(address indexed token, uint256 price);
}

Type Casting with Interfaces

contract Example {
    function interact(address tokenAddress) public {
        // Cast address to interface
        IERC20 token = IERC20(tokenAddress);

        // Now can call interface methods
        uint256 balance = token.balanceOf(msg.sender);
    }

    function getAddress(IERC20 token) public pure returns (address) {
        // Cast interface to address
        return address(token);
    }
}

Best Practices

1. Use Standard Interfaces

// Good - use established standards
interface IERC20 { ... }
interface IERC721 { ... }
interface IERC1155 { ... }

2. Keep Interfaces Focused

// Good - single purpose interfaces
interface IOwnable {
    function owner() external view returns (address);
    function transferOwnership(address newOwner) external;
}

interface IPausable {
    function paused() external view returns (bool);
    function pause() external;
    function unpause() external;
}

// Avoid - kitchen sink interface
interface IEverything {
    // Too many unrelated functions
}

3. Document Interface Functions

interface IVault {
    /// @notice Deposits tokens into the vault
    /// @param amount The amount to deposit
    /// @return shares The number of shares minted
    function deposit(uint256 amount) external returns (uint256 shares);

    /// @notice Withdraws tokens from the vault
    /// @param shares The number of shares to burn
    /// @return amount The amount of tokens returned
    function withdraw(uint256 shares) external returns (uint256 amount);
}

4. Version Interfaces

interface IVaultV1 {
    function deposit(uint256 amount) external;
}

interface IVaultV2 is IVaultV1 {
    function depositWithReferral(uint256 amount, address referrer) external;
}

5. Include Events and Errors

interface IMarket {
    // Events for off-chain tracking
    event Listed(uint256 indexed id, address indexed seller, uint256 price);
    event Sold(uint256 indexed id, address indexed buyer);

    // Errors for clear failure reasons
    error NotListed(uint256 id);
    error InsufficientPayment(uint256 required, uint256 provided);

    // Functions
    function list(uint256 id, uint256 price) external;
    function buy(uint256 id) external payable;
}

Next Steps