Back to Tools
TEMPLATE10 min read2025-06-23

ERC-20 Token Contract Template — Production-Ready Implementation

A production-ready ERC-20 token contract including: fixed supply, permit (EIP-2612), capped minting, vesting integration hooks, and OpenZeppelin best practices.

Enterprise Ready

Built for production environments.

Fast Results

Save hours of manual planning.

Professional

Trusted by blockchain teams.

Blockchain Tool

Interactive planning, estimation, calculations and enterprise-grade blockchain utilities.

AI

Assisted

24/7

Available

Tool Assistance

Ready to Deploy Your Token?

Get expert guidance on ERC-20 token development and deployment.

Tool Workspace

Use, understand, and apply the results

Tool Overview

A production-ready ERC-20 token contract including: fixed supply, permit (EIP-2612), capped minting, vesting integration hooks, and OpenZeppelin best practices.

Key Result

A production-ready ERC-20 token contract using OpenZeppelin 0.8.24 with fixed max supply, EIP-2612 permit, EIP-5805 votes, owner-controlled minting, pausable transfers, and vesting support. Includes: minting with MAX_SUPPLY cap, permanent mint disable, minter role management, emergency pause/unpause, and vesting vault contract with cliff and linear release. Always use OpenZeppelin for ERC-20 — their contracts are audited by leading firms and used in $100B+ of DeFi.

Example
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";

/**
 * @title ProtocolToken
 * @notice Production-ready ERC-20 governance token
 * @dev Includes: fixed max supply, EIP-2612 permit, EIP-5805 votes, 
 *      owner-controlled minting, pausable transfers, vesting support
 */
contract ProtocolToken is ERC20, ERC20Permit, ERC20Votes, Ownable, Pausable {
    
    /// @notice Maximum total supply that can ever be minted
    uint256 public immutable MAX_SUPPLY;
    
    /// @notice Whether minting has been permanently disabled
    bool public mintingDisabled;
    
    /// @notice Addresses authorized to mint new tokens
    mapping(address => bool) public minters;
    
    // ============ ERRORS ============
    error MaxSupplyExceeded(uint256 requested, uint256 available);
    error MintingDisabled();
    error NotMinter(address caller);
    error ZeroAddress();
    error ZeroAmount();
    
    // ============ EVENTS ============
    event MinterAdded(address indexed minter);
    event MinterRemoved(address indexed minter);
    event MintingPermanentlyDisabled();
    
    /**
     * @param name_ Token name
     * @param symbol_ Token symbol  
     * @param maxSupply_ Maximum total supply (in wei, with 18 decimals)
     * @param initialHolder_ Address receiving initial supply allocation
     * @param initialAmount_ Initial mint amount
     */
    constructor(
        string memory name_,
        string memory symbol_,
        uint256 maxSupply_,
        address initialHolder_,
        uint256 initialAmount_
    ) ERC20(name_, symbol_) ERC20Permit(name_) Ownable(msg.sender) {
        if (initialHolder_ == address(0)) revert ZeroAddress();
        if (initialAmount_ > maxSupply_) revert MaxSupplyExceeded(initialAmount_, maxSupply_);
        
        MAX_SUPPLY = maxSupply_;
        
        if (initialAmount_ > 0) {
            _mint(initialHolder_, initialAmount_);
        }
    }
    
    // ============ MINTING ============
    
    /**
     * @notice Mint new tokens (up to MAX_SUPPLY)
     * @dev Only callable by addresses with minter role
     */
    function mint(address to, uint256 amount) external {
        if (mintingDisabled) revert MintingDisabled();
        if (!minters[msg.sender] && msg.sender != owner()) revert NotMinter(msg.sender);
        if (to == address(0)) revert ZeroAddress();
        if (amount == 0) revert ZeroAmount();
        
        uint256 available = MAX_SUPPLY - totalSupply();
        if (amount > available) revert MaxSupplyExceeded(amount, available);
        
        _mint(to, amount);
    }
    
    /**
     * @notice Permanently disable minting (irreversible)
     * @dev Once called, no more tokens can ever be minted
     */
    function disableMintingPermanently() external onlyOwner {
        mintingDisabled = true;
        emit MintingPermanentlyDisabled();
    }
    
    // ============ MINTER MANAGEMENT ============
    
    function addMinter(address minter) external onlyOwner {
        if (minter == address(0)) revert ZeroAddress();
        minters[minter] = true;
        emit MinterAdded(minter);
    }
    
    function removeMinter(address minter) external onlyOwner {
        minters[minter] = false;
        emit MinterRemoved(minter);
    }
    
    // ============ EMERGENCY PAUSE ============
    
    function pause() external onlyOwner {
        _pause();
    }
    
    function unpause() external onlyOwner {
        _unpause();
    }
    
    // ============ OVERRIDES ============
    
    function _update(
        address from,
        address to,
        uint256 amount
    ) internal override(ERC20, ERC20Votes) whenNotPaused {
        super._update(from, to, amount);
    }
    
    function nonces(address owner_) 
        public view override(ERC20Permit, Nonces) 
        returns (uint256) 
    {
        return super.nonces(owner_);
    }
}

Vesting Contract Integration

Example
// VestingVault.sol — holds tokens and releases per schedule
contract VestingVault is Ownable {
    
    IERC20 public immutable token;
    
    struct VestingSchedule {
        address beneficiary;
        uint256 totalAmount;
        uint256 startTime;
        uint256 cliffDuration;  // seconds
        uint256 vestingDuration; // total vesting seconds
        uint256 released;
        bool revocable;
        bool revoked;
    }
    
    mapping(bytes32 => VestingSchedule) public schedules;
    bytes32[] public scheduleIds;
    
    constructor(address _token) Ownable(msg.sender) {
        token = IERC20(_token);
    }
    
    function createSchedule(
        address beneficiary,
        uint256 amount,
        uint256 cliffSeconds,
        uint256 vestingSeconds,
        bool revocable
    ) external onlyOwner returns (bytes32 scheduleId) {
        token.transferFrom(msg.sender, address(this), amount);
        
        scheduleId = keccak256(abi.encodePacked(beneficiary, block.timestamp, amount));
        
        schedules[scheduleId] = VestingSchedule({
            beneficiary: beneficiary,
            totalAmount: amount,
            startTime: block.timestamp,
            cliffDuration: cliffSeconds,
            vestingDuration: vestingSeconds,
            released: 0,
            revocable: revocable,
            revoked: false
        });
        
        scheduleIds.push(scheduleId);
        emit ScheduleCreated(scheduleId, beneficiary, amount);
    }
    
    function release(bytes32 scheduleId) external {
        VestingSchedule storage s = schedules[scheduleId];
        require(msg.sender == s.beneficiary || msg.sender == owner(), "Unauthorized");
        require(!s.revoked, "Revoked");
        
        uint256 releasable = _computeReleasable(s);
        require(releasable > 0, "Nothing to release");
        
        s.released += releasable;
        token.transfer(s.beneficiary, releasable);
        
        emit TokensReleased(scheduleId, releasable);
    }
    
    function _computeReleasable(VestingSchedule storage s) internal view returns (uint256) {
        if (block.timestamp < s.startTime + s.cliffDuration) return 0;
        
        uint256 elapsed = block.timestamp - s.startTime;
        uint256 vested;
        
        if (elapsed >= s.vestingDuration) {
            vested = s.totalAmount;
        } else {
            vested = s.totalAmount * elapsed / s.vestingDuration;
        }
        
        return vested - s.released;
    }
    
    event ScheduleCreated(bytes32 scheduleId, address beneficiary, uint256 amount);
    event TokensReleased(bytes32 scheduleId, uint256 amount);
}

FAQ

Common Questions

1

Always use OpenZeppelin. Their contracts are audited by the leading security firms, used in $100B+ of DeFi, and maintain backwards-compatible updates. Writing your own ERC-20 from scratch introduces risk without benefit. Customize via inheritance (add features on top of OZ base), not by rewriting the core standard.

Tool Assistance

Ready to Deploy Your Token?

Get expert guidance on ERC-20 token development and deployment.