Appendix G: Token Standards Reference

This appendix provides a working reference for the five most important Ethereum token standards as of 2025. Each entry includes the complete interface, function-by-function documentation, events, common implementation pitfalls, and a minimal usage example. Readers working through Parts III, V, and VI will find themselves returning to these pages repeatedly.

All interfaces follow the Ethereum Improvement Proposal (EIP) specifications. Implementations should use audited libraries (OpenZeppelin, Solmate) rather than writing from scratch in production. The examples here are for understanding, not for deployment without audit.


G.1 ERC-20: Fungible Token Standard

EIP: EIP-20 Finalized: September 2017 Purpose: Standard interface for fungible (interchangeable) tokens Use cases: Governance tokens (UNI, COMP), stablecoins (USDT, USDC, DAI), utility tokens, wrapped assets (WETH, WBTC)

G.1.1 Full Interface

// SPDX-License-Identifier: MIT
interface IERC20 {
    // Returns the total supply of tokens in existence
    function totalSupply() external view returns (uint256);

    // Returns the token balance of `account`
    function balanceOf(address account) external view returns (uint256);

    // Transfers `amount` tokens from caller to `to`
    // Returns true on success
    // MUST emit Transfer event
    // MUST revert if caller's balance < amount
    function transfer(address to, uint256 amount) external returns (bool);

    // Returns the remaining number of tokens that `spender`
    // is allowed to spend on behalf of `owner`
    function allowance(address owner, address spender) external view returns (uint256);

    // Sets `amount` as the allowance of `spender` over caller's tokens
    // Returns true on success
    // MUST emit Approval event
    function approve(address spender, uint256 amount) external returns (bool);

    // Transfers `amount` tokens from `from` to `to` using allowance mechanism
    // `amount` is deducted from caller's allowance
    // Returns true on success
    // MUST emit Transfer event
    function transferFrom(address from, address to, uint256 amount) external returns (bool);

    // Emitted when `value` tokens are moved from `from` to `to`
    // `from` == address(0) indicates minting
    // `to` == address(0) indicates burning
    event Transfer(address indexed from, address indexed to, uint256 value);

    // Emitted when `owner` sets allowance for `spender` to `value`
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

G.1.2 Optional Metadata Extension

interface IERC20Metadata is IERC20 {
    function name() external view returns (string memory);     // e.g., "Uniswap"
    function symbol() external view returns (string memory);   // e.g., "UNI"
    function decimals() external view returns (uint8);         // e.g., 18
}

Important

decimals() is purely cosmetic. The contract stores all values as integers. A balance of 1000000000000000000 with decimals() == 18 displays as 1.0 in a wallet. USDT and USDC use 6 decimals. Always check decimals before performing arithmetic across different tokens.

G.1.3 Common Gotchas

Issue Description Mitigation
Approve race condition If Alice has approved Bob for 100 tokens and changes approval to 50, Bob can front-run by spending 100 before the new approval lands, then spending 50 more (total: 150). Use increaseAllowance() / decreaseAllowance() (OpenZeppelin extension). Or set approval to 0 first, then to the new value.
Non-standard return values Some tokens (USDT on Ethereum) do not return bool from transfer(). Calling code that expects a return value will revert. Use OpenZeppelin's SafeERC20 library, which handles missing return values.
Fee-on-transfer tokens Some tokens deduct a fee on every transfer. If you transfer 100, the recipient receives 97. Contracts that assume balanceOf(this) increased by exactly the transferred amount will break. Measure balance before and after transfer: uint256 received = balanceOf(this) - balanceBefore;
Rebasing tokens Tokens like stETH change balances automatically. A balance of 1000 today may be 1001 tomorrow. Contracts storing snapshots of balances will desync. Use wrapped versions (wstETH) that do not rebase, or track shares instead of absolute amounts.
Decimals assumption Hardcoding 10**18 when a token uses 6, 8, or 0 decimals causes catastrophic arithmetic errors. Always query decimals() and compute scaling factors dynamically.
Zero-address transfers The spec says transfer to address(0) should revert in transfer(), but some implementations use it for burns. Explicitly check for address(0) in contracts receiving tokens.

G.1.4 Example: Minimal ERC-20 Implementation

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract GovernanceToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("GovernanceToken", "GOV") {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }
}

This deploys a token with name() = "GovernanceToken", symbol() = "GOV", decimals() = 18 (OpenZeppelin default), and mints the full supply to the deployer.

G.1.5 Notable Extensions

  • ERC-20 Permit (EIP-2612): Allows approvals via off-chain signatures, eliminating the need for a separate approve() transaction. Users sign a typed message; the spender submits it on-chain. Saves gas and improves UX.
  • ERC-20 Votes (OpenZeppelin): Adds delegation and vote-weight checkpointing for governance tokens. Used by UNI, COMP, and ENS.
  • ERC-20 Snapshot (OpenZeppelin): Records balances at specific block numbers, enabling fair airdrop snapshots and governance vote snapshots.

G.2 ERC-721: Non-Fungible Token (NFT) Standard

EIP: EIP-721 Finalized: January 2018 Purpose: Standard interface for non-fungible (unique) tokens Use cases: Digital art (CryptoPunks, Art Blocks), gaming items (Axie Infinity), domain names (ENS), real-world asset certificates, event tickets

G.2.1 Full Interface

interface IERC721 {
    // Returns the number of tokens owned by `owner`
    function balanceOf(address owner) external view returns (uint256);

    // Returns the owner of token `tokenId`
    // MUST revert if tokenId does not exist
    function ownerOf(uint256 tokenId) external view returns (address);

    // Safely transfers token from `from` to `to`
    // If `to` is a contract, MUST call onERC721Received()
    // MUST revert if caller is not owner/approved/operator
    function safeTransferFrom(address from, address to, uint256 tokenId) external;

    // Overload with additional data passed to onERC721Received()
    function safeTransferFrom(
        address from, address to, uint256 tokenId, bytes calldata data
    ) external;

    // Transfers token — does NOT check if recipient can handle NFTs
    // Caller must be owner/approved/operator
    function transferFrom(address from, address to, uint256 tokenId) external;

    // Approves `to` to manage token `tokenId`
    // Only one approved address per token at a time
    // Setting to address(0) clears approval
    function approve(address to, uint256 tokenId) external;

    // Sets/revokes `operator` as approved for ALL of caller's tokens
    function setApprovalForAll(address operator, bool approved) external;

    // Returns the approved address for token `tokenId`
    function getApproved(uint256 tokenId) external view returns (address);

    // Returns true if `operator` is approved for all of `owner`'s tokens
    function isApprovedForAll(address owner, address operator) external view returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
}

G.2.2 Metadata Extension

interface IERC721Metadata is IERC721 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);

    // Returns a URI pointing to JSON metadata for token `tokenId`
    // JSON schema: { "name": "...", "description": "...", "image": "..." }
    function tokenURI(uint256 tokenId) external view returns (string memory);
}

Metadata JSON Standard (EIP-721 Metadata JSON Schema):

{
    "name": "Crypto Artifact #42",
    "description": "A unique digital artifact from the Genesis collection.",
    "image": "ipfs://QmYx6GsYAKnNzZ9A6NvEKV9nf8BC9...",
    "attributes": [
        { "trait_type": "Rarity", "value": "Legendary" },
        { "trait_type": "Power", "value": 95, "display_type": "number" },
        { "trait_type": "Generation", "value": 1 }
    ]
}

G.2.3 Enumerable Extension

interface IERC721Enumerable is IERC721 {
    // Returns total number of tokens in existence
    function totalSupply() external view returns (uint256);

    // Returns the tokenId at `index` in the global token list
    function tokenByIndex(uint256 index) external view returns (uint256);

    // Returns the tokenId at `index` of tokens owned by `owner`
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);
}

⚠️ Gas Warning: The Enumerable extension significantly increases gas costs for minting and transfers (roughly 2x). Many modern collections omit it and instead rely on off-chain indexing (The Graph, Alchemy NFT API) to enumerate tokens. Use only if on-chain enumeration is a hard requirement.

G.2.4 Common Gotchas

Issue Description Mitigation
Unsafe transfer to contracts transferFrom() does not check if the receiving contract can handle NFTs. Tokens sent to incompatible contracts are permanently locked. Always use safeTransferFrom(). Receiving contracts must implement IERC721Receiver.
Off-chain metadata dependency tokenURI() typically points to IPFS or HTTP. If the server goes down or IPFS is unpinned, the metadata is lost. The NFT itself (the on-chain tokenId) persists, but what it "means" disappears. Use IPFS with pinning services (Pinata, nft.storage). Some projects store metadata fully on-chain (Base64-encoded SVG).
Approval persists after transfer Approvals are cleared on transfer in the standard implementation, but setApprovalForAll is NOT cleared. An operator approved for "all tokens" retains approval even after the owner acquires new tokens. Be cautious with setApprovalForAll. Revoke approvals on marketplaces you no longer use.
Re-entrancy via onERC721Received safeTransferFrom calls external code (onERC721Received on the recipient). If the recipient is a malicious contract, it can re-enter your contract mid-transfer. Follow checks-effects-interactions pattern. Use ReentrancyGuard.

G.3 ERC-1155: Multi-Token Standard

EIP: EIP-1155 Finalized: June 2019 Purpose: Single contract managing multiple token types (fungible AND non-fungible) Use cases: Gaming (items, currencies, unique loot in one contract), collections with editions, mixed fungible/non-fungible asset portfolios

G.3.1 Full Interface

interface IERC1155 {
    // Returns the balance of token type `id` for `account`
    function balanceOf(address account, uint256 id) external view returns (uint256);

    // Batch version — returns array of balances
    // `accounts` and `ids` arrays must be equal length
    function balanceOfBatch(
        address[] calldata accounts, uint256[] calldata ids
    ) external view returns (uint256[] memory);

    // Sets/revokes `operator` approval for ALL of caller's tokens
    function setApprovalForAll(address operator, bool approved) external;

    // Returns true if `operator` is approved for all of `account`'s tokens
    function isApprovedForAll(address account, address operator) external view returns (bool);

    // Transfers `amount` of token type `id` from `from` to `to`
    // If `to` is a contract, MUST call onERC1155Received()
    function safeTransferFrom(
        address from, address to, uint256 id, uint256 amount, bytes calldata data
    ) external;

    // Batch transfer — `ids` and `amounts` arrays must be equal length
    // If `to` is a contract, MUST call onERC1155BatchReceived()
    function safeBatchTransferFrom(
        address from, address to,
        uint256[] calldata ids, uint256[] calldata amounts,
        bytes calldata data
    ) external;

    event TransferSingle(
        address indexed operator, address indexed from,
        address indexed to, uint256 id, uint256 value
    );

    event TransferBatch(
        address indexed operator, address indexed from,
        address indexed to, uint256[] ids, uint256[] values
    );

    event ApprovalForAll(address indexed account, address indexed operator, bool approved);

    // Emitted when the URI for token type `id` changes
    event URI(string value, uint256 indexed id);
}

G.3.2 Metadata Extension

interface IERC1155MetadataURI is IERC1155 {
    // Returns the URI for token type `id`
    // Clients MUST replace {id} in the URI with the lowercase hex token ID
    // Example: "https://game.example/api/item/{id}.json"
    //   for id = 314592 -> "https://game.example/api/item/0x04cc60.json"
    function uri(uint256 id) external view returns (string memory);
}

G.3.3 Key Design Advantages over ERC-20 + ERC-721

Feature ERC-20 + ERC-721 ERC-1155
Deploy cost One contract per token type One contract for all types
Batch transfer Requires N separate transactions Single safeBatchTransferFrom call
Mixed types Cannot mix fungible and non-fungible in one contract Fungible (supply > 1) and non-fungible (supply == 1) coexist
Gas per transfer Higher (separate storage per contract) Lower (~30-50% savings on batch ops)
Approval granularity ERC-20: per-token-amount; ERC-721: per-tokenId or all All-or-nothing (no per-id approval)

G.3.4 Common Gotchas

Issue Description Mitigation
No per-ID approval Unlike ERC-721, you cannot approve a specific operator for a specific token ID. It is all-or-nothing via setApprovalForAll. Design around this limitation. Use wrapper contracts if fine-grained approval is needed.
ID encoding conventions Many implementations pack type info into the upper bits and index into the lower bits of the uint256 ID. There is no standard for this encoding. Document your ID scheme explicitly. Use consistent masking.
Receiver hook differences safeTransferFrom calls onERC1155Received; safeBatchTransferFrom calls onERC1155BatchReceived. Implementing one but not the other causes failures. Always implement both receiver hooks in contracts that accept ERC-1155 tokens.

G.4 ERC-4626: Tokenized Vault Standard

EIP: EIP-4626 Finalized: March 2022 Purpose: Standard interface for yield-bearing vaults (deposit assets, receive shares) Use cases: Yield aggregators (Yearn V3), lending pools (Aave aTokens wrapper), liquid staking (stETH wrappers), auto-compounding strategies

G.4.1 Full Interface

// ERC-4626 extends ERC-20 — vault shares ARE ERC-20 tokens
interface IERC4626 is IERC20 {
    // --- Vault Info ---

    // Returns the address of the underlying asset token (e.g., USDC)
    function asset() external view returns (address);

    // Returns total amount of the underlying asset managed by the vault
    function totalAssets() external view returns (uint256);

    // --- Conversion ---

    // Returns shares that would be minted for `assets` amount deposited
    // MUST NOT revert (for preview purposes)
    function convertToShares(uint256 assets) external view returns (uint256);

    // Returns assets that would be received for `shares` amount redeemed
    // MUST NOT revert (for preview purposes)
    function convertToAssets(uint256 shares) external view returns (uint256);

    // --- Deposit ---

    // Returns max assets that `receiver` can deposit
    function maxDeposit(address receiver) external view returns (uint256);

    // Preview: how many shares for this deposit
    function previewDeposit(uint256 assets) external view returns (uint256);

    // Deposits `assets` of underlying token, mints shares to `receiver`
    // MUST emit Deposit event
    function deposit(uint256 assets, address receiver) external returns (uint256 shares);

    // --- Mint ---

    // Returns max shares that `receiver` can mint
    function maxMint(address receiver) external view returns (uint256);

    // Preview: how many assets needed to mint this many shares
    function previewMint(uint256 shares) external view returns (uint256);

    // Mints exactly `shares` vault tokens to `receiver` by depositing assets
    function mint(uint256 shares, address receiver) external returns (uint256 assets);

    // --- Withdraw ---

    // Returns max assets that `owner` can withdraw
    function maxWithdraw(address owner) external view returns (uint256);

    // Preview: how many shares burned for this withdrawal
    function previewWithdraw(uint256 assets) external view returns (uint256);

    // Burns shares from `owner`, sends `assets` of underlying to `receiver`
    function withdraw(
        uint256 assets, address receiver, address owner
    ) external returns (uint256 shares);

    // --- Redeem ---

    // Returns max shares that `owner` can redeem
    function maxRedeem(address owner) external view returns (uint256);

    // Preview: how many assets received for this redemption
    function previewRedeem(uint256 shares) external view returns (uint256);

    // Burns `shares` from `owner`, sends underlying assets to `receiver`
    function redeem(
        uint256 shares, address receiver, address owner
    ) external returns (uint256 assets);

    event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
    event Withdraw(
        address indexed sender, address indexed receiver,
        address indexed owner, uint256 assets, uint256 shares
    );
}

G.4.2 Share Calculation Mechanics

The core accounting formula:

shares = (assets * totalShares) / totalAssets
assets = (shares * totalAssets) / totalShares

Worked example: 1. Vault is empty. Alice deposits 1,000 USDC. She receives 1,000 shares (1:1 ratio on first deposit). 2. Vault earns 100 USDC yield. totalAssets = 1,100 USDC. totalShares = 1,000. 3. Bob deposits 1,000 USDC. His shares = (1,000 * 1,000) / 1,100 = 909.09 shares. totalAssets = 2,100. totalShares = 1,909.09. 4. Alice redeems her 1,000 shares. She receives (1,000 * 2,100) / 1,909.09 = 1,100 USDC. She earned 100 USDC (her proportional share of the yield). 5. Bob redeems his 909.09 shares. He receives (909.09 * 1,000) / 909.09 = 1,000 USDC. He earned nothing — he deposited after the yield was earned.

G.4.3 Common Gotchas

Issue Description Mitigation
Inflation attack First depositor can donate tokens directly to the vault (not through deposit), inflating totalAssets without minting shares. When the next user deposits, they receive fewer shares than expected, and the attacker redeems for a profit. Use OpenZeppelin's virtual shares/assets offset: initialize with a virtual deposit (e.g., 1e6 virtual shares and assets).
Rounding direction convertToShares should round DOWN (fewer shares for depositor). convertToAssets should round DOWN (fewer assets for redeemer). Inconsistent rounding creates arbitrage. Follow EIP-4626 rounding rules exactly. OpenZeppelin's implementation handles this correctly.
preview vs. actual previewDeposit and previewRedeem may not account for fees or slippage. The actual deposit/redeem may differ from the preview. Treat preview functions as estimates. Add slippage tolerance in frontends.
Async withdrawal Some vaults (e.g., staking vaults with unbonding periods) cannot fulfill withdraw instantly. ERC-4626 assumes synchronous operations. Use ERC-7540 (async vault standard) for vaults with withdrawal delays. Or return 0 from maxWithdraw during cooldown periods.

G.5 ERC-4337: Account Abstraction

EIP: EIP-4337 Finalized: March 2023 Purpose: Enable smart contract wallets with programmable validation logic, without protocol-level changes Use cases: Social recovery wallets, multi-sig wallets with flexible policies, gasless transactions (sponsored by dApps), session keys, batch transactions

Key Insight: ERC-4337 is not a token standard — it is an account standard. It is included here because it fundamentally changes how users interact with all of the token standards above.

G.5.1 Architecture Overview

ERC-4337 introduces a parallel transaction mempool for UserOperations — pseudo-transactions that describe what a smart contract wallet wants to do. The system has four core components:

  1. Smart Contract Wallet (Account): A contract that validates signatures and executes transactions. Replaces the EOA (Externally Owned Account) as the user's account.
  2. EntryPoint: A singleton contract (deployed once per chain) that processes UserOperations. All bundlers submit to the same EntryPoint.
  3. Bundler: An off-chain node that collects UserOperations, bundles them into a regular transaction, and submits to the EntryPoint.
  4. Paymaster (optional): A contract that sponsors gas on behalf of the user. Enables gasless transactions.

G.5.2 UserOperation Structure

struct UserOperation {
    address sender;              // The smart contract wallet address
    uint256 nonce;               // Anti-replay nonce (managed by EntryPoint)
    bytes initCode;              // Factory address + calldata to deploy wallet (empty if already deployed)
    bytes callData;              // The actual operation to execute (encoded function call)
    uint256 callGasLimit;        // Gas for the main execution call
    uint256 verificationGasLimit; // Gas for the validation step
    uint256 preVerificationGas;  // Gas overhead for bundler processing
    uint256 maxFeePerGas;        // EIP-1559 max fee
    uint256 maxPriorityFeePerGas; // EIP-1559 priority fee
    bytes paymasterAndData;      // Paymaster address + data (empty if self-paying)
    bytes signature;             // Signature validated by the wallet contract
}

G.5.3 Account Interface

interface IAccount {
    // Called by EntryPoint to validate the UserOperation
    // MUST verify the signature is valid for the given UserOp
    // Returns validationData:
    //   0 = valid
    //   1 = signature failure
    //   or packed (authorizer, validUntil, validAfter) for time-bounded validity
    function validateUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 missingAccountFunds
    ) external returns (uint256 validationData);
}

G.5.4 EntryPoint Interface (Key Functions)

interface IEntryPoint {
    // Main execution function — called by bundlers
    // Validates and executes a batch of UserOperations
    function handleOps(
        UserOperation[] calldata ops,
        address payable beneficiary  // Receives the gas refund
    ) external;

    // Deposit ETH to the EntryPoint for a given account
    // Used to pre-fund gas payments
    function depositTo(address account) external payable;

    // Returns the deposit balance for `account`
    function balanceOf(address account) external view returns (uint256);

    // Account can withdraw excess deposit
    function withdrawTo(
        address payable withdrawAddress,
        uint256 withdrawAmount
    ) external;

    // Get the nonce for a given sender and key
    function getNonce(address sender, uint192 key) external view returns (uint256);
}

G.5.5 Paymaster Interface

interface IPaymaster {
    // Called during validation to determine if paymaster will sponsor
    // Returns context (passed to postOp) and validationData
    function validatePaymasterUserOp(
        UserOperation calldata userOp,
        bytes32 userOpHash,
        uint256 maxCost
    ) external returns (bytes memory context, uint256 validationData);

    // Called after execution — paymaster can perform accounting
    // `mode` indicates if the main execution succeeded or reverted
    // `actualGasCost` is the actual gas consumed
    function postOp(
        PostOpMode mode,
        bytes calldata context,
        uint256 actualGasCost
    ) external;

    enum PostOpMode {
        opSucceeded,   // UserOp succeeded
        opReverted,    // UserOp reverted (paymaster still pays)
        postOpReverted // postOp itself reverted on first call
    }
}

G.5.6 Execution Flow

1. User constructs UserOperation (off-chain)
2. User sends UserOp to bundler (via alt mempool, not standard tx mempool)
3. Bundler validates UserOp locally (calls simulateValidation)
4. Bundler bundles multiple UserOps into one transaction
5. Bundler calls EntryPoint.handleOps(ops, beneficiary)
6. For each UserOp, EntryPoint:
   a. If initCode is set, deploys the wallet via factory
   b. Calls account.validateUserOp() — wallet verifies signature
   c. If paymaster is set, calls paymaster.validatePaymasterUserOp()
   d. Executes account.callData (the actual user operation)
   e. If paymaster is set, calls paymaster.postOp()
   f. Refunds unused gas to bundler

G.5.7 Common Gotchas

Issue Description Mitigation
Storage access restrictions During validation, the account and paymaster can only access their own storage. This prevents DoS attacks where a single storage slot change invalidates many UserOps. Design validation logic to be self-contained. Move external checks to the execution phase.
Gas estimation difficulty Three separate gas limits (callGasLimit, verificationGasLimit, preVerificationGas) make estimation harder than traditional transactions. Use bundler's eth_estimateUserOperationGas RPC method. Add buffer (10-20%) to estimates.
Paymaster trust A paymaster that agrees to sponsor a UserOp during validation could refuse to pay in postOp by reverting. The EntryPoint handles this via the postOpReverted mode. Use reputable paymasters. The EntryPoint's staking mechanism penalizes misbehaving paymasters.
Nonce management ERC-4337 uses a 2D nonce scheme: key (uint192) and sequence (uint64). Using different keys allows parallel UserOps; same-key UserOps must be sequential. Use key 0 for sequential operations. Use unique keys for independent operations that can execute in any order.
Counterfactual deployment The wallet address is determined by the initCode before deployment. If the factory or init parameters change, the address changes — and any assets sent to the original address are inaccessible. Use CREATE2-based factories with deterministic addresses. Verify the address before sending funds.

G.6 Standards Comparison Matrix

Feature ERC-20 ERC-721 ERC-1155 ERC-4626 ERC-4337
Type Fungible token Non-fungible token Multi-token Vault (extends ERC-20) Account abstraction
Each token identical? Yes No (each unique) Configurable Shares are fungible N/A
Batch operations No (1 tx per transfer) No Yes No Yes (via callData)
Approval model Amount-based Per-token or operator Operator only Amount-based (inherited) Programmable
Safe transfer No (but SafeERC20 library) Yes (receiver hook) Yes (receiver hook) No (inherited from ERC-20) N/A
Metadata Optional (name, symbol, decimals) URI per token URI with {id} substitution Inherited from ERC-20 N/A
Gas (deploy) ~1M ~2M ~2.5M ~1.5M ~3-5M (account + factory)
Gas (transfer) ~50K ~70K ~50K (single), ~30K/item (batch) ~80K (includes yield calc) ~100-300K (via EntryPoint)
OpenZeppelin support Yes Yes Yes Yes Yes (via ERC-4337 utilities)

Note

Gas estimates are approximate and vary significantly with implementation complexity, chain congestion, and compiler optimization settings. Measure with your specific code.


G.7 Emerging Standards Worth Watching

Standard Status Purpose
ERC-6551 (Token Bound Accounts) Draft Gives every ERC-721 its own smart contract wallet. NFTs can own other tokens.
ERC-7540 (Async Vaults) Draft Extends ERC-4626 for vaults with asynchronous deposit/withdrawal (e.g., staking with unbonding).
ERC-7579 (Modular Accounts) Draft Standardizes modules (validators, executors, hooks) for ERC-4337 smart accounts.
ERC-3525 (Semi-Fungible Token) Final Tokens with value (like ERC-20) inside unique slots (like ERC-721). Useful for financial instruments.
ERC-5192 (Minimal Soulbound) Final Non-transferable NFTs (soulbound tokens). For credentials, reputation, identity.

For production use, never implement these standards from scratch. Use audited, battle-tested libraries:

OpenZeppelin Contracts (MIT License) - @openzeppelin/contracts/token/ERC20/ERC20.sol - @openzeppelin/contracts/token/ERC721/ERC721.sol - @openzeppelin/contracts/token/ERC1155/ERC1155.sol - @openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol - Includes extensions: Pausable, Burnable, Votes, Permit, Snapshot, Enumerable - npm: npm install @openzeppelin/contracts

Solmate (AGPL-3.0 License) - Gas-optimized implementations (typically 10-30% cheaper than OpenZeppelin) - Less safety-oriented (no overflow checks in some cases — relies on Solidity 0.8+ built-in checks) - Preferred for gas-critical applications - GitHub: transmissions11/solmate

Solady (MIT License) - Even more aggressively gas-optimized (uses assembly) - Excellent for L2 deployments where calldata costs dominate - GitHub: Vectorized/solady

Choose OpenZeppelin for safety-first development, Solmate for gas optimization with readable code, and Solady when every gas unit matters.