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.
// 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
// 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);
}