ERC Token Standards skill for Codex

ERC-20, 721 and 1155 composed from audited parts. Codex picks the right token standard and OpenZeppelin 5 extensions for the spec, writes the exact _update, nonces and supportsInterface overrides Solidity demands, enforces supply caps and mint rights, and serves metadata wallets can read. It also ports v4 hooks like _beforeTokenTransfer to v5 and tests conformance down to interface IDs and permit signatures.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- erc-token-standards

Installs to ~/.agents/skills/erc-token-standards/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $erc-token-standards, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Implements ERC-20, ERC-721 and ERC-1155 tokens on OpenZeppelin Contracts 5.x - choosing extensions (Permit, Votes, Capped, Burnable, Enumerable, URIStorage, Supply, ERC-2981 royalties), resolving v5 _update and supportsInterface override conflicts, minting and supply rules, metadata JSON and {id} URIs - and tests standard conformance. Use when asked to create a token, NFT collection or multi-token contract, add permit or voting, set royalties, fix override compile errors, or migrate token code from OpenZeppelin 4 to 5.

Use it when

  • You need a fungible token, NFT collection or multi-token contract
  • Compile errors appear about _update, nonces or supportsInterface overrides
  • Token code written for OpenZeppelin 4 must move to version 5
  • Royalties, permit approvals or voting power need adding to a token

Use something else when

How to check the result

Tests cover every transfer path, ERC-6093 revert and event, a fuzzed supply cap, receiver-hook behaviour and supportsInterface for each implemented interface ID.

The mistake it prevents

Shipping ERC20Votes without telling holders to self-delegate, so every balance shows zero voting power at the first proposal.

~/.agents/skills/erc-token-standards/SKILL.md

--- name: erc-token-standards description: Implements ERC-20, ERC-721 and ERC-1155 tokens on OpenZeppelin Contracts 5.x - choosing extensions (Permit, Votes, Capped, Burnable, Enumerable,… license: MIT ---

A token contract is correct when it is a thin composition of audited OpenZeppelin parts, custom logic lives only in the _update hook or minting functions, supply and minting rights are explicit and tested, metadata resolves for every token ID, and wallets and marketplaces read it without special cases.

Inspect first

  1. Read AGENTS.md and check the installed OpenZeppelin version: cat lib/openzeppelin-contracts/package.json | grep '"version"' (Foundry) or npm ls @openzeppelin/contracts. v5 removed the _beforeTokenTransfer and _afterTokenTransfer hooks, increaseAllowance, and the security/ folder; code written for v4 will not compile.
  2. Ask (or infer from the spec): fixed or mintable supply? Who mints? Burnable? Pausable? Governance voting? Gasless approvals? Royalties? Upgradeable?
  3. For upgradeable tokens use @openzeppelin/contracts-upgradeable and $upgradeable-contracts.

Pick the standard

NeedStandard
Fungible currency, points, governance tokenERC-20
Unique items, each with its own owner and metadataERC-721
Many token types in one contract, some fungible (game items, editions, tickets)ERC-1155
Tokenized vault shares over an ERC-20 assetERC-4626 (extends ERC-20)

Pick extensions (OpenZeppelin 5.x)

WantERC-20ERC-721ERC-1155
Signature approvals (gasless)ERC20Permit--
On-chain voting with checkpointsERC20VotesERC721Votes-
Hard supply capERC20Cappedcustom check in mintERC1155Supply + check
Holders can burnERC20BurnableERC721BurnableERC1155Burnable
Emergency stopERC20PausableERC721PausableERC1155Pausable
List all token IDs on-chain-ERC721Enumerable (costly; prefer an indexer)-
Per-token URI-ERC721URIStorageERC1155URIStorage
Royalty info-ERC2981ERC2981

OpenZeppelin’s Contracts Wizard generates the correct override boilerplate for any combination; when it is reachable, use it as a cross-check for override lists.

ERC-20 with permit and votes

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";

contract GovToken is ERC20, ERC20Permit, ERC20Votes, Ownable {
    uint256 public constant MAX_SUPPLY = 100_000_000e18;
    error CapExceeded(uint256 requested, uint256 remaining);

    constructor(address owner_) ERC20("Gov Token", "GOV") ERC20Permit("Gov Token") Ownable(owner_) {}

    function mint(address to, uint256 amount) external onlyOwner {
        uint256 remaining = MAX_SUPPLY - totalSupply();
        if (amount > remaining) revert CapExceeded(amount, remaining);
        _mint(to, amount);
    }

    // Required by Solidity because both parents define these.
    function _update(address from, address to, uint256 value) internal override(ERC20, ERC20Votes) {
        super._update(from, to, value);
    }

    function nonces(address owner) public view override(ERC20Permit, Nonces) returns (uint256) {
        return super.nonces(owner);
    }
}
  • ERC20Votes only counts delegated balances. Holders must call delegate(self) once; say so in the UI and docs or voting power looks like zero.
  • Change decimals by overriding decimals(); all amounts in code and tests then use that scale. Stablecoin-like tokens commonly use 6.
  • Approval race: v5 removed increaseAllowance. Front ends should set allowance to the exact amount needed, or use permit.

ERC-721

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import {ERC2981} from "@openzeppelin/contracts/token/common/ERC2981.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract Collection is ERC721, ERC721Enumerable, ERC2981, Ownable {
    uint256 public constant MAX_SUPPLY = 10_000;
    uint256 private _nextId = 1;
    string private _base;
    error SoldOut();

    constructor(address owner_, string memory base_) ERC721("Collection", "COL") Ownable(owner_) {
        _base = base_;
        _setDefaultRoyalty(owner_, 500); // 5%, denominator 10_000
    }

    function mint(address to) external onlyOwner returns (uint256 id) {
        id = _nextId;
        if (id > MAX_SUPPLY) revert SoldOut();
        _nextId = id + 1;          // effects before the receiver callback
        _safeMint(to, id);
    }

    function _baseURI() internal view override returns (string memory) { return _base; }

    function _update(address to, uint256 tokenId, address auth)
        internal override(ERC721, ERC721Enumerable) returns (address)
    { return super._update(to, tokenId, auth); }

    function _increaseBalance(address account, uint128 value)
        internal override(ERC721, ERC721Enumerable)
    { super._increaseBalance(account, value); }

    function supportsInterface(bytes4 id)
        public view override(ERC721, ERC721Enumerable, ERC2981) returns (bool)
    { return super.supportsInterface(id); }
}
  • _safeMint calls onERC721Received on contract recipients: update counters before it (reentrancy). _mint skips the check and can strand tokens in contracts.
  • ERC721Enumerable makes every transfer markedly more expensive. Skip it unless on-chain enumeration is a hard requirement; indexers do this off-chain.
  • ERC-2981 only reports royalties. Marketplaces decide whether to pay them.
  • To hide unrevealed metadata, point the base URI at a placeholder and emit BatchMetadataUpdate (ERC-4906, from IERC4906) when revealing so marketplaces refresh.

ERC-1155

import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import {ERC1155Supply} from "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";

contract Items is ERC1155, ERC1155Supply /*, Ownable */ {
    constructor() ERC1155("https://api.example.com/items/{id}.json") {}

    function _update(address from, address to, uint256[] memory ids, uint256[] memory values)
        internal override(ERC1155, ERC1155Supply)
    { super._update(from, to, ids, values); }
}
  • {id} is replaced by clients with the token ID as 64 lowercase hex characters, zero padded, no 0x. Your server or storage must serve files with those names.
  • Batch transfers call onERC1155BatchReceived; recipients that do not implement it make the transfer revert.

Metadata

ERC-721 / ERC-1155 JSON served at tokenURI / uri:

{
  "name": "Item #42",
  "description": "One line about the item.",
  "image": "ipfs://bafy.../42.png",
  "attributes": [{ "trait_type": "Rarity", "value": "Rare" }]
}
  • Prefer content-addressed storage (ipfs://, Arweave) for permanence; an HTTPS API you can change is a centralization point users should know about.
  • name/description/image come from the standards’ metadata schema; attributes is a marketplace convention, not part of the EIPs.

v4 to v5 migration

v4v5
_beforeTokenTransfer / _afterTokenTransferOverride _update
Ownable() with implicit ownerOwnable(initialOwner)
security/Pausable.sol, security/ReentrancyGuard.solutils/Pausable.sol, utils/ReentrancyGuard.sol
increaseAllowance / decreaseAllowanceRemoved; use approve or permit
Counters libraryRemoved; use a plain uint256
Revert stringsCustom errors from ERC-6093 (ERC20InsufficientBalance, …)
_exists(id)_ownerOf(id) != address(0)

Never upgrade OZ majors in a deployed upgradeable token without $upgradeable-contracts: v5 uses ERC-7201 namespaced storage, which is incompatible with v4 layouts.

Testing conformance

  • Unit: mint, transfer, approve, transferFrom, burn, every revert with its ERC-6093 error, every event (Transfer, Approval, TransferSingle, TransferBatch).
  • Supply: fuzz that totalSupply never exceeds the cap and equals the sum of balances across a fixed actor set (invariant test).
  • Receivers: mint to a contract without the receiver hook (expect revert for safe paths).
  • Interfaces: assertTrue(nft.supportsInterface(type(IERC721).interfaceId)), and the same for IERC2981 and IERC1155.
  • Permit: sign with vm.sign over the EIP-712 digest; test expired deadline and reused nonce.

Output contract

**Token:** GovToken (ERC-20, 18 decimals) on OZ 5.x
**Extensions:** Permit, Votes, Ownable; cap 100M enforced in mint
**Minting:** owner only (intended owner: Safe 0x...); no burn
**Metadata:** n/a | base URI ipfs://... ; royalty 5% to 0x...
**Tests:** 27 passing incl. permit, delegation, cap fuzz; supportsInterface checked
**User-facing notes:** holders must self-delegate to vote

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "ERC Token Standards"
  short_description: "ERC-20, 721 and 1155 composed from audited parts"
  default_prompt: "$erc-token-standards Create an ERC-20 governance token with permit, voting and a 100M cap"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem