ERC-6093: Custom Errors for ERC-20 / 721 / 1155
ERC-6093 standardises a small set of typed error declarations that ERC-20, ERC-721, and ERC-1155 implementations should use for their common revert conditions: insufficient balance, missing approval, invalid sender, invalid receiver. Before this ERC, every implementation rolled its own — "ERC20: insufficient allowance" strings vs. InsufficientAllowance(...) custom errors vs. silent reverts. Indexers, wallets, and explorers all had to special-case each shape. ERC-6093 picks one and asks every token contract to converge.
OpenZeppelin v5+ ships ERC-6093 errors by default; Solady has them too; most new token contracts use them. The standard is a vocabulary, not a runtime contract.
Required Errors (selection)
// ERC-20
error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed);
error ERC20InvalidSender(address sender);
error ERC20InvalidReceiver(address receiver);
error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed);
error ERC20InvalidApprover(address approver);
error ERC20InvalidSpender(address spender);
// ERC-721
error ERC721InvalidOwner(address owner);
error ERC721NonexistentToken(uint256 tokenId);
error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner);
error ERC721InvalidSender(address sender);
error ERC721InvalidReceiver(address receiver);
error ERC721InsufficientApproval(address operator, uint256 tokenId);
error ERC721InvalidApprover(address approver);
error ERC721InvalidOperator(address operator);
// ERC-1155
error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId);
error ERC1155InvalidSender(address sender);
error ERC1155InvalidReceiver(address receiver);
error ERC1155MissingApprovalForAll(address operator, address owner);
error ERC1155InvalidApprover(address approver);
error ERC1155InvalidOperator(address operator);
error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength);The 4-byte selector (first four bytes of keccak256 of the signature) becomes the error's wire identifier; clients decode it from the revert data.
Neo Equivalent: Named Exception Convention
NeoVM doesn't have Solidity's typed error machinery — every revert is a Throw with a string (or any StackItem) payload. Two practical equivalents:
A. Standardised throw strings. Adopt a uniform vocabulary like "NEP17:InsufficientBalance" or "NEP11:NonexistentToken". Indexers parse the prefix; humans read the suffix. Cheap, no new infrastructure.
B. Structured throw payloads. Throw a Map<string, object> with fields like { "code": "InsufficientBalance", "balance": 100, "needed": 200 }. Higher cost (storage of a structured payload), richer for tooling.
Pattern A is the de-facto convention in current Neo C# devpack contracts; this mirror page proposes formalising the prefix and code list to match ERC-6093 one-for-one.
| ERC-6093 (Solidity) | Neo C# Equivalent (Pattern A) | Notes |
|---|---|---|
revert ERC20InsufficientBalance(...) | throw new Exception("NEP17:InsufficientBalance") | Selector ↔ string code; arguments dropped (payload in string only if Pattern B) |
revert ERC20InvalidSender(addr) | throw new Exception("NEP17:InvalidSender") | |
revert ERC721NonexistentToken(id) | throw new Exception("NEP11:NonexistentToken") | |
revert ERC721IncorrectOwner(...) | throw new Exception("NEP11:IncorrectOwner") | |
revert ERC1155InvalidArrayLength(...) | Neo NEP-11 doesn't have ERC-1155 batch shape; equivalent applies if you implement an ERC-1155-style port |
Proposed Vocabulary for Neo
| Code | When To Throw |
|---|---|
NEP17:InsufficientBalance | transfer/burn with amount > balanceOf(from) |
NEP17:InvalidSender | transfer from UInt160.Zero (or other invalid sender) |
NEP17:InvalidReceiver | transfer to UInt160.Zero (or invalid receiver) |
NEP17:NoAuth | Runtime.CheckWitness(from) returned false |
NEP17:AmountNegative | amount < 0 |
NEP17:CallbackRejected | OnNEP17Payment reverted on the recipient |
NEP11:NonexistentToken | tokenId not found |
NEP11:IncorrectOwner | Caller claimed ownership of a token they don't own |
NEP11:InvalidSender / NEP11:InvalidReceiver | Same as NEP-17 |
NEP11:NoAuth | Owner witness not found |
NEP11:CallbackRejected | OnNEP11Payment reverted |
Migration Notes
For dApps porting ERC-20 contracts via neo-solc, the Solidity error declarations compile to Throw instructions on NeoVM — but the typed error Foo(...) payload is lost (no wire-level encoding equivalent). Either:
- Strip the error declarations and use plain
require(cond, "NEP17:Code")— string-only path, idiomatic for current Neo tooling. - Keep the error declarations for readability and use an off-chain pre-processor that maps the fired selector to the matching Neo string code.
For new contracts authored in Neo C#, adopt the vocabulary above as a starting point. The mirror page exists to close the "what should I throw?" ambiguity that every devpack contract author hits in their first week.
