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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- erc-token-standards Installs to ~/.agents/skills/erc-token-standards/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install erc-token-standards The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project erc-token-standards Installs to .agents/skills/erc-token-standards/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/erc-token-standards.zipunzip -o erc-token-standards.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) erc-token-standards PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The token must be upgradeable behind a proxy: use upgradeable-contracts
- You are writing non-token protocol logic such as vaults or escrow: use solidity-contracts
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.
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
- Read
AGENTS.mdand check the installed OpenZeppelin version:cat lib/openzeppelin-contracts/package.json | grep '"version"'(Foundry) ornpm ls @openzeppelin/contracts. v5 removed the_beforeTokenTransferand_afterTokenTransferhooks,increaseAllowance, and thesecurity/folder; code written for v4 will not compile. - Ask (or infer from the spec): fixed or mintable supply? Who mints? Burnable? Pausable? Governance voting? Gasless approvals? Royalties? Upgradeable?
- For upgradeable tokens use
@openzeppelin/contracts-upgradeableand$upgradeable-contracts.
Pick the standard
| Need | Standard |
|---|---|
| Fungible currency, points, governance token | ERC-20 |
| Unique items, each with its own owner and metadata | ERC-721 |
| Many token types in one contract, some fungible (game items, editions, tickets) | ERC-1155 |
| Tokenized vault shares over an ERC-20 asset | ERC-4626 (extends ERC-20) |
Pick extensions (OpenZeppelin 5.x)
| Want | ERC-20 | ERC-721 | ERC-1155 |
|---|---|---|---|
| Signature approvals (gasless) | ERC20Permit | - | - |
| On-chain voting with checkpoints | ERC20Votes | ERC721Votes | - |
| Hard supply cap | ERC20Capped | custom check in mint | ERC1155Supply + check |
| Holders can burn | ERC20Burnable | ERC721Burnable | ERC1155Burnable |
| Emergency stop | ERC20Pausable | ERC721Pausable | ERC1155Pausable |
| List all token IDs on-chain | - | ERC721Enumerable (costly; prefer an indexer) | - |
| Per-token URI | - | ERC721URIStorage | ERC1155URIStorage |
| Royalty info | - | ERC2981 | ERC2981 |
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);
}
}
ERC20Votesonly counts delegated balances. Holders must calldelegate(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 usepermit.
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); }
}
_safeMintcallsonERC721Receivedon contract recipients: update counters before it (reentrancy)._mintskips the check and can strand tokens in contracts.ERC721Enumerablemakes 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, fromIERC4906) 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, no0x. 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/imagecome from the standards’ metadata schema;attributesis a marketplace convention, not part of the EIPs.
v4 to v5 migration
| v4 | v5 |
|---|---|
_beforeTokenTransfer / _afterTokenTransfer | Override _update |
Ownable() with implicit owner | Ownable(initialOwner) |
security/Pausable.sol, security/ReentrancyGuard.sol | utils/Pausable.sol, utils/ReentrancyGuard.sol |
increaseAllowance / decreaseAllowance | Removed; use approve or permit |
Counters library | Removed; use a plain uint256 |
| Revert strings | Custom 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
totalSupplynever 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 forIERC2981andIERC1155. - Permit: sign with
vm.signover 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.yaml
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.