
Openzeppelin Contracts
- 3 installs
- 4 repo stars
- Updated February 25, 2026
- hairyf/blockchain-skills
Build EVM smart contracts using OpenZeppelin's audited library for access control, ERC token standards, and upgradeable patterns.
About
Reference for OpenZeppelin Contracts, the secure smart-contract library for EVM covering access control, ERC20/721/1155/4626/6909 tokens, upgradeable variants, governance, and utilities. A developer uses it when writing or reviewing Solidity contracts that inherit from OpenZeppelin base contracts.
- Covers Ownable, AccessControl RBAC, AccessManager, and TimelockController
- Includes upgradeable contracts, ERC-4337 account abstraction, and Governor governance
Openzeppelin Contracts by the numbers
- 3 all-time installs (skills.sh)
- Ranked #390 of 479 Web3 & Blockchain skills by installs in the Skillselion catalog
- Data as of Jul 13, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hairyf/blockchain-skills --skill openzeppelin-contractsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| repo stars | ★ 4 |
| Last updated | February 25, 2026 |
| Repository | hairyf/blockchain-skills ↗ |
What it does
Build EVM smart contracts using OpenZeppelin's audited library for access control, ERC token standards, and upgradeable patterns.
Files
Skill based on OpenZeppelin Contracts (docs as of 2026-02-09), generated from sources/openzeppelin.OpenZeppelin Contracts is a library for secure smart contract development on EVM. Use via inheritance (e.g. ERC20, AccessControl) or using for (e.g. ECDSA, Math). Covers access control (Ownable, RBAC, AccessManager, TimelockController), tokens (ERC20, ERC721, ERC1155, ERC4626, ERC6909), upgradeable variants, and utilities (crypto, math, introspection, structures, storage).
Core References
| Topic | Description | Reference |
|---|---|---|
| Overview | Library usage, inheritance, extending contracts | core-overview |
| Access Control | Ownable, AccessControl (RBAC), AccessManager, TimelockController | core-access-control |
| Tokens | Token standards and when to use ERC20/721/1155/4626/6909 | core-tokens |
| ERC20 | Fungible tokens, decimals, transfer, supply | core-erc20 |
| ERC721 | Non-fungible tokens, URI storage, minting | core-erc721 |
| ERC1155 | Multi-token (fungible + NFT), batch ops, safe transfer to contracts | core-erc1155 |
| ERC4626 | Tokenized vaults, shares vs assets, inflation attack mitigation | core-erc4626 |
| ERC6909 | Multi-asset (no batch/callbacks), granular approvals, extensions | core-erc6909 |
| ERC20 Supply | Creating supply with _mint and _update, fixed and reward patterns | core-erc20-supply |
Features
Upgradeable
| Topic | Description | Reference |
|---|---|---|
| Upgradeable Contracts | contracts-upgradeable, initializers, namespaced storage | features-upgradeable |
Governance & Accounts
| Topic | Description | Reference |
|---|---|---|
| Account Abstraction | ERC-4337 stack: UserOperation, EntryPoint, Bundler, Paymaster | features-account-abstraction |
| Governor | On-chain governance, ERC20Votes, quorum, timelock, proposal lifecycle | features-governance |
| Multisig | ERC-7913 signers, threshold and weighted multisig with Account | features-multisig |
| Smart Accounts | ERC-4337 Account, signers, factory, UserOp, batched execution | features-accounts |
| EOA Delegation | EIP-7702 delegation to contracts, SignerEIP7702, authorization | features-eoa-delegation |
Utilities
| Topic | Description | Reference |
|---|---|---|
| Utilities | ECDSA, MerkleProof, Math, ERC165, structures, StorageSlot, Multicall | features-utilities |
Best Practices
| Topic | Description | Reference |
|---|---|---|
| Backwards Compatibility | Semantic versioning, storage layout, safe overrides | best-practices-backwards-compatibility |
| Extending Contracts | Inheritance, overrides, super, security when customizing | best-practices-extending-contracts |
| EOA Restriction | Why not to restrict to EOAs only; use access control instead | best-practices-eoa-restriction |
Generation Info
- Source:
sources/openzeppelin - Git SHA:
7bcb9603a8894dc1c78751c31dfead8789712fb4 - Generated: 2026-02-09
Backwards Compatibility
OpenZeppelin Contracts use semantic versioning. Patch and minor are generally backwards compatible; major releases are not (especially storage and upgrades).
API
- Backwards-compatible releases: mostly additions or internal changes. Exceptions: security fixes (breaking changes noted in changelog), draft/pre-final ERCs in
draft-*.sol(no guarantee), and virtual/override surface—only a subset of functions are designed to be overridden. - Struct members with underscore prefix are internal; access only via library/contract APIs.
- Revert error format and data are not guaranteed stable unless specified.
Storage Layout
- Minor and patch preserve storage layout. Upgrading a proxy from one minor to another is safe for layout; new state may need initializing.
- Major releases: storage layout is not compatible; do not upgrade a live contract across major versions.
- Use OpenZeppelin Upgrades Plugins or CLI to validate storage when upgrading.
Overrides
- Prefer overriding only documented extension points. Overriding other functions may depend on internals and break on updates.
- When Solidity reports ambiguous inherited functions, add an override that calls
super.functionName(). - Custom overrides (especially hooks) can invalidate security assumptions; revalidate when upgrading the library.
Key Points
- Never upgrade a deployed proxy across major versions (e.g. 4.x → 5.x).
- Draft ERCs (
draft-*.sol) can change in breaking ways. - When extending contracts, re-check overrides and storage after upgrading OpenZeppelin.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/backwards-compatibility.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/extending-contracts.adoc
-->
Restricting to EOAs Only
Do not restrict functions to “EOAs only” (e.g. by requiring msg.sender to have no code). This pattern is unsafe and breaks composability.
Why it’s discouraged
- Composability: Smart wallets (e.g. Gnosis Safe), multisigs, and other contracts cannot call the function.
- No real security: The check can be bypassed by calling from a contract’s constructor (no code at the address yet) or from an address that will have a contract deployed later.
- Ambiguity of “has code”:
address.code.length > 0only means the address currently has code. The opposite does not mean the address is an EOA. Counterexamples: - Contract currently being constructed (no code yet at that address).
- Address where a contract will be deployed later.
- Address where a contract used to be (destroyed by
SELFDESTRUCT; code is cleared at end of transaction). - Same transaction: address is still considered to have code until the transaction ends, even if
SELFDESTRUCTis in the same tx.
What to do instead
- Allow any caller and enforce authorization with access control (roles, ownership, or capability checks).
- If you need “only a human” or “only one key,” use account abstraction (e.g. ERC-4337) or signature checks bound to a specific account, not “is EOA.”
Key points
- Restricting to EOAs is brittle and bypassable; prefer role-based or signature-based checks.
- Use OpenZeppelin’s access control and signature utilities instead of
extcodesize/code-length checks.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/faq.adoc
-->
Extending Contracts
OpenZeppelin contracts are used via inheritance (contract MyToken is ERC20). Libraries (e.g. ECDSA, Math) use using Lib for Type;, not inheritance. Use overrides to change or extend behavior; be aware of security when customizing.
Overriding
Replace a parent function by defining one with the same signature. To completely disable a function, override it to revert:
function revokeRole(bytes32 role, address account) public override {
revert("Revocation disabled");
}You cannot remove the function from the ABI; reverting on all calls is the usual approach.
Extending with super
Call super.functionName(...) to invoke the parent’s implementation and then add your logic (extra checks, events, state). Use this when you want to preserve original behavior and add to it:
function revokeRole(bytes32 role, address account) public override {
require(role != DEFAULT_ADMIN_ROLE, "Cannot revoke default admin");
super.revokeRole(role, account);
}super runs the immediate parent’s version; with multiple inheritance, Solidity’s C3 linearization determines the order.
Security
- Custom overrides, especially of hooks or internal functions, can break assumptions and introduce vulnerabilities. Review the base contract source when overriding.
- Internal usage of functions may change between library versions; do not rely on undocumented internal call patterns. Re-validate overrides when upgrading OpenZeppelin.
- Prefer official extensions (e.g.
AccessControlDefaultAdminRules) over ad-hoc overrides when they match your needs; they are designed and tested with the base contracts.
Key points
- Use inheritance for contracts; use
using forfor libraries. - Override to restrict or extend; use
superto keep and extend behavior. - Document and audit overrides; upgrade carefully and re-check against new base behavior.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/extending-contracts.adoc
-->
Access Control
Control who can perform actions (mint, upgrade, etc.) using Ownable, role-based access, or a central AccessManager.
Ownable
- Single
owner; useonlyOwnermodifier. Set at deployment viainitialOwner. transferOwnership(newOwner)andrenounceOwnership().- Prefer
Ownable2Stepwhen transferring ownership: new owner must callacceptOwnership(). - Owner can be a contract (e.g. Gnosis Safe, DAO).
AccessControl (RBAC)
- Define roles as
bytes32(e.g.keccak256("MINTER_ROLE")). UseonlyRole(role)modifier. - Grant/revoke via
grantRole/revokeRole; only the role’s admin can do this.DEFAULT_ADMIN_ROLEis the default admin for all roles. - Use
_grantRolein constructor for initial setup; usegrantRole/revokeRolefor dynamic assignment. - For enumerating role members on-chain, use
AccessControlEnumerable(getRoleMemberCount, getRoleMember, getRoleMembers).
AccessManager
- Central contract storing permissions for many contracts. Targets are (contract, function selector); access is limited to one role per target.
- Managed contracts inherit
AccessManagedand use therestrictedmodifier; setinitialAuthorityto the AccessManager address. - Roles are
uint64(0 = ADMIN_ROLE). UsegrantRole(role, account, executionDelay),setTargetFunctionRole(target, selectors, role). - Supports grant delay and execution delay; delayed operations must be
scheduled then executed. UsesetTargetClosed(target, true)for incident response.
TimelockController
- Proxy governed by proposers and executors; operations scheduled through it are subject to a minimum delay.
- Use as owner/admin of contracts to enforce a delay on maintenance (e.g. upgrades). Roles:
PROPOSER_ROLE,EXECUTOR_ROLE,CANCELLER_ROLE,DEFAULT_ADMIN_ROLE. getMinDelay()/updateDelay()(only callable by the timelock itself).
Key Points
- Prefer RBAC over single owner when you need granular permissions (minter vs burner vs admin).
- Use AccessControlDefaultAdminRules for safer DEFAULT_ADMIN_ROLE (single account, 2-step transfer with delay).
- For multi-contract systems, AccessManager centralizes permissions and supports delays and emergency close.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/access-control.adoc
-->
ERC1155
Multi-token standard: one contract represents many token ids; each id can be fungible (balance > 1) or non-fungible (balance 1). Use for games, mixed fungible/non-fungible assets, gas-efficient multi-token systems.
Usage
import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
contract GameItems is ERC1155 {
constructor() ERC1155("https://game.example/api/item/{id}.json") {
_mint(msg.sender, 0, 10000, ""); // Gold (fungible)
_mint(msg.sender, 1, 100, ""); // Silver
_mint(msg.sender, 2, 1, ""); // Thor's Hammer (NFT)
}
}balanceOf(account, id): balance ofidforaccount. Nodecimals; ids are distinct.safeTransferFrom(from, to, id, amount, data)andsafeBatchTransferFrom(from, to, ids[], amounts[], data)for transfers. Use batch for multiple ids in one tx.balanceOfBatch(accounts[], ids[])returns multiple balances in one call.- Internal:
_mint(account, id, amount, data),_mintBatch(account, ids[], amounts[], data).
Sending to contracts
Transfers to contracts revert with ERC1155InvalidReceiver(address) unless the receiver implements IERC1155Receiver. Use ERC1155Holder so the contract can receive and optionally implement logic to send tokens out:
import { ERC1155Holder } from "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
contract MyHolder is ERC1155Holder {
// implement onERC1155Received / onERC1155BatchReceived if needed
// and functions to transfer tokens out
}Key points
- Single contract holds all token state; batch ops reduce gas vs multiple ERC20/721 contracts.
- Metadata: optional
IERC1155MetadataURI;uri(id)can use{id}placeholder (clients replace with 64-char hex, no 0x). - For on-chain metadata use
ERC1155URIStorageor overrideuri()(e.g. Base64 data URI); costly.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/erc1155.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/tokens.adoc
-->
Creating ERC-20 Supply
ERC-20 does not define how supply is created. OpenZeppelin uses internal _mint(account, amount) so that extensions can implement custom supply logic while keeping totalSupply and balances consistent and emitting Transfer correctly.
Fixed supply
Mint once in the constructor to the deployer (or a designated address):
contract ERC20FixedSupply is ERC20 {
constructor() ERC20("Fixed", "FIX") {
_mint(msg.sender, 1000);
}
}Do not write to totalSupply or balances directly; use _mint so events and invariants stay correct.
Reward or conditional minting
Use _mint from any function (e.g. reward to block proposer, staking rewards, airdrops). Restrict with access control (e.g. onlyRole(MINTER_ROLE)) or custom rules:
function mintMinerReward() public {
_mint(block.coinbase, 1000);
}Hooking transfers (_update)
Override _update(from, to, amount) to run logic on every transfer, mint, or burn (mint: from == address(0), burn: to == address(0)). For example, mint a reward to the block proposer on every transfer:
function _update(address from, address to, uint256 amount) internal override {
super._update(from, to, amount);
if (from != address(0)) {
_mint(block.coinbase, 1000);
}
}Use with care: minting on every transfer can have economic and gas implications.
Key points
- All supply changes should go through
_mint(and_burnif applicable); never touchtotalSupply/balances directly. - Restrict who can mint (roles, owner, or specific conditions) to avoid unbounded supply.
- For more complex supply (cap, timelock, governance), combine
_mint/_updatewith access control and optional extensions (e.g. ERC20Votes for governance).
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/erc20-supply.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/erc20.adoc
-->
ERC20
Fungible token: balances, transfer, approve. Use for currency, voting, staking.
Construction
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract GLDToken is ERC20 {
constructor() ERC20("Gold", "GLD") {
_mint(msg.sender, 1000 * 10 ** decimals());
}
}- Name, symbol, and optional decimals (default 18) come from ERC20/ERC20Metadata.
- Create supply in constructor via
_mintor add a minter role and mint later; see erc20-supply docs for patterns (fixed, capped, mintable).
Decimals
decimals()is for display only; all math is in raw units. To send “5” tokens use5 * (10 ** decimals()).- Override
decimals()to use a value other than 18.
Transfer and Approval
transfer(to, amount),approve(spender, amount),transferFrom(from, to, amount).- Use extensions (e.g. ERC20Permit for gasless approvals) as needed.
Key Points
- Restrict minting/burning with access control (e.g. AccessControl + MINTER_ROLE).
- Use the official package; do not copy-paste. Only used code is deployed.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/erc20.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/erc20-supply.adoc
-->
ERC4626
Standard interface for token vaults: users deposit underlying assets and receive shares; shares are burned to withdraw assets. Use for yield-bearing tokens, lending vaults, wrappers. OpenZeppelin provides a base implementation with virtual offset to mitigate inflation attacks.
Usage
import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MyVault is ERC4626 {
constructor(IERC20 asset_) ERC4626(asset_) ERC20("Vault Shares", "vASSET") {}
}deposit(assets, receiver)/mint(shares, receiver): deposit assets and get shares (or mint exact shares).withdraw(assets, receiver, owner)/redeem(shares, receiver, owner): burn shares and get assets.previewDeposit(assets),previewMint(shares),previewWithdraw(assets),previewRedeem(shares): view functions that must match actual share/asset amounts (rounding down for user when appropriate). Integrators and UIs rely on these.
Inflation attack and virtual offset
An attacker can donate assets to an empty vault to skew the share rate so the next depositor gets rounded to 0 shares. Defend with:
1. Virtual offset: ERC4626 uses virtual shares and virtual assets so the effective rate when the vault is empty is high (e.g. 10^offset), making small donations unprofitable. 2. Decimals: Use more decimals for shares than the asset (e.g. asset 18, shares 18+offset) so the initial rate is safer and rounding loss is bounded.
Overriding _decimalsOffset() (or constructor for custom vaults) sets the offset; default implementation provides protection.
Fees
Keep ERC4626 compliance: preview* must match actual amounts. For deposit fees: user pays assets, receiver gets previewDeposit(assets) shares; take fee from the assets before crediting shares. For withdraw fees: user burns previewWithdraw(assets) shares and receives assets; fee is added on top in share terms inside previewWithdraw. Emit Deposit/Withdraw with the values that reflect user-paid assets and received shares (including fees) so events describe the two exchange rates (buy-in vs exit).
Key points
- Always use the library’s vault or extend it with minimal overrides; preserve preview accuracy.
- First depositor / empty-vault handling is critical; virtual offset is the recommended defense.
- For fee vaults, implement fees in deposit/mint and/or withdraw/redeem while keeping previews and events consistent with the spec.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/erc4626.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/tokens.adoc
-->
ERC6909
Multi-asset standard aimed at lower gas and simpler design than ERC-1155: one contract, multiple token ids, no batch operations and no transfer callbacks. Use when you need multiple token types in one contract and don’t need ERC-1155’s batch or safe-transfer semantics.
Differences from ERC-1155
- No batch ops: Only single-id
balanceOf(account, id)andtransfer(to, id, amount)(and operatortransferFrom). NobalanceOfBatchorsafeBatchTransferFrom. - No callbacks: Transfers to contracts do not require
onERC1155Received; tokens can be sent to any address. - Approvals: Operator approvals can be global (all ids) or per-id amounts (ERC-20 style).
Usage
import { ERC6909 } from "@openzeppelin/contracts/token/ERC6909/ERC6909.sol";
import { ERC6909Metadata } from "@openzeppelin/contracts/token/ERC6909/extensions/ERC6909Metadata.sol";
contract GameItems is ERC6909, ERC6909Metadata {
constructor() ERC6909Metadata("Game Items", "GIT") {
_mint(msg.sender, 0, 10000); // id 0: fungible
_mint(msg.sender, 1, 1); // id 1: NFT
}
}- ERC6909: base balance and transfer; internal
_mint(account, id, amount). - ERC6909Metadata: optional
name,symbol, anddecimals(id)(per-id decimals for fungible ids). - ERC6909ContentURI: optional
contentURI(id)for metadata. - ERC6909TokenSupply: optional total supply per id (
totalSupply(id)).
Base implementation does not track total supply; use ERC6909TokenSupply if needed. No content URI in base; add ERC6909ContentURI for metadata by id.
Key points
- Prefer ERC-6909 when you want a single contract for many ids and can give up batching and receiver callbacks for gas and simplicity.
- Use
ERC6909Metadatafor decimals per id; useERC6909ContentURIfor off-chain or on-chain metadata by id.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/erc6909.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/tokens.adoc
-->
ERC721
Non-fungible tokens: each token has a unique tokenId; use for collectibles, in-game items, deeds.
Construction
import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import { ERC721URIStorage } from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract GameItem is ERC721, ERC721URIStorage {
constructor() ERC721("GameItem", "ITM") {}
function awardItem(address to, string memory tokenURI) public {
uint256 tokenId = _nextTokenId();
_safeMint(to, tokenId);
_setTokenURI(tokenId, tokenURI);
}
function _baseURI() internal pure override returns (string memory) {
return "https://game.example/item-id-";
}
}- Use
ERC721URIStoragefor per-token metadata and_setTokenURI.tokenURI(tokenId)should resolve to a JSON (name, description, image, etc.) per EIP-721. - No
decimalsin ERC721; tokens are indivisible. - Restrict minting (e.g.
onlyRole(MINTER_ROLE)) in production.
Key Points
- Prefer
_safeMintso receivers that implementIERC721Receiverare notified. - Metadata can be off-chain (URL) or on-chain (e.g. Base64 Data URI via utils Base64); off-chain allows changes by the deployer.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/erc721.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/tokens.adoc
-->
OpenZeppelin Contracts Overview
OpenZeppelin Contracts is a library for secure smart contract development. Use it via inheritance (e.g. contract MyToken is ERC20) or libraries via using X for type (e.g. using ECDSA for bytes32).
Usage
- Install:
npm install @openzeppelin/contracts(Hardhat) orforge install OpenZeppelin/openzeppelin-contracts(Foundry). Use tagged releases, notmaster. - Import and inherit: Only the contracts and functions you use are deployed; no need to worry about gas bloat.
- Do not copy-paste or modify library code; use the installed package as-is for security.
Extending Contracts
- Override parent behavior with
overrideand optionally callsuper.functionName()to extend rather than replace. - Only a subset of functions are designed to be overridden; overriding others may rely on internal details and break across releases.
- Custom overrides, especially to hooks, can introduce security risks—review against the source you are customizing.
Key Points
- Contracts are expected to be used via inheritance; libraries use
using for. - Semantic versioning: patch/minor are generally backwards compatible; major versions are not (especially upgrade/storage).
- NPM tags:
latest= audited,dev= final but unaudited,next= release candidates.
<!-- Source references:
- https://docs.openzeppelin.com/contracts/
- sources/openzeppelin/docs/modules/ROOT/pages/index.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/extending-contracts.adoc
-->
Tokens
Tokens are on-chain representations of value or rights. OpenZeppelin implements standard interfaces; choose by fungibility and use case.
Standards
- ERC20: Fungible (balances, transfer, approve). Use for currency, voting rights, staking. See core-erc20.
- ERC721: Non-fungible (unique tokenId, ownerOf, transferFrom). Use for collectibles, in-game items, deeds.
- ERC1155: Multi-token (fungible and non-fungible in one contract, batch operations). Use for games, mixed assets.
- ERC4626: Tokenized vault (shares vs assets). Use for yield-bearing or wrapped assets.
- ERC6909: Multi-asset (multiple “ids” per contract, minimal interface). Lightweight multi-token.
Key Points
- Token contract = smart contract; “sending tokens” = calling methods that update balances or ownership.
- Fungible: “how much”; non-fungible: “which one”. ERC1155/6909 support both in one contract.
- Always use the library’s implementations via inheritance; restrict minting/burning with access control (e.g. onlyRole(MINTER_ROLE)).
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/tokens.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/erc20.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/erc721.adoc
-->
Account Abstraction (ERC-4337 Overview)
ERC-4337 defines an account-abstraction stack without protocol changes: UserOperations go through an alternative mempool and are executed via an EntryPoint. Accounts can use arbitrary validation (not only ECDSA) and benefit from batching and gas sponsorship.
Components
- UserOperation (e.g.
PackedUserOperation): pseudo-transaction withsender,nonce,initCode(factory + data),callData,accountGasLimits(verification + call gas),preVerificationGas,gasFees,paymasterAndData,signature. Bundlers use gas fields to cover costs and charge users. - EntryPoint: singleton contract that runs
validateUserOpon the account then executes the op. Trusted by the account. Same address across many networks for the canonical EntryPoint. - Bundler: off-chain infra that collects UserOps, calls EntryPoint’s
handleOps(ops, beneficiary), pays gas and is refunded during execution. Beneficiary receives collected fees. - Account: implements validation (e.g.
validateUserOp) and execution (e.g. fallback, ERC-7821 batch, or custom). Must conform to the expected validation interface. - Factory: creates accounts;
initCode = abi.encodePacked(factoryAddress, factoryCalldata). Deployer chooses salt/params so address is deterministic. - Paymaster: optional; sponsors gas or lets users pay in ERC-20. See community-contracts paymasters for implementation.
Use ERC4337Utils for working with the UserOperation struct and related ERC-4337 values.
Validation (ERC-7562)
Bundlers call validateUserOp on the sender; ERC-7562 restricts what accounts can do during validation so bundlers are protected from arbitrary state changes. Accounts that only read their own storage for signature checks are typically fine; cross-account or heavy validation can violate ERC-7562 and may require a private bundler.
Key points
- UserOp flows: Bundler → EntryPoint → Account (validate then execute). Factory used when account not yet deployed (
initCode). - For building an account or factory, see the Accounts and EOA Delegation references. For paymasters, see OpenZeppelin community-contracts.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/account-abstraction.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/accounts.adoc
-->
Smart Accounts (ERC-4337)
OpenZeppelin’s Account implements ERC-4337 user-operation handling. You provide signature validation via an AbstractSigner (implement _rawSignatureValidation). Use for account abstraction: gas sponsoring, batched execution, and custom validation (ECDSA, P256, RSA, EIP-7702, ERC-7913, multisig).
Signers
- SignerECDSA: EOA signatures.
- SignerP256: secp256r1 (passkeys, FIDO, secure enclaves).
- SignerRSA: PKI / X.509.
- SignerEIP7702: EOA delegated to this account (EIP-7702).
- SignerERC7913: generic ERC-7913 (verifier + key).
- MultiSignerERC7913 / MultiSignerERC7913Weighted: threshold/weighted multisig.
Implement _rawSignatureValidation(bytes32 hash, bytes memory signature) and return true if valid. Use ERC-7739 to bind signatures to account address/chainId and avoid replay across accounts; expose isValidSignature(hash, signature) returning IERC1271.isValidSignature.selector when valid.
Setup and factory
Accounts are often deployed by a factory via initCode in the UserOperation (factory address + calldata). Use the Clones library for minimal clones; include the owner/signer in the clone salt so the address is deterministic and frontrunning is prevented. After deployment, call an initializer (e.g. initializeECDSA(signer)) so the account has a signer set; leaving it uninitialized can make it unusable.
Inherit ERC721Holder and ERC1155Holder if the account should receive ERC-721/ERC-1155 tokens (they require receiver callbacks).
Batched execution (ERC-7821)
ERC-7821 adds batched execution. Override _erc7821AuthorizedExecutor(caller, mode, executionData) to allow the entry point (and optionally self) to execute batches. Encode batch as ERC-7579-style execution data (call type 0x01 for batch, default exec type). Use execute(mode, batch) from the account; mode includes batch selector and exec type.
UserOperation flow
1. Prepare: Set sender, nonce, callData, accountGasLimits (verificationGasLimit, callGasLimit), preVerificationGas, gasFees, paymasterAndData. If account not deployed, set initCode = abi.encodePacked(factory, factoryCalldata). 2. Sign: Hash the UserOp with EIP-712 (EntryPoint domain, PackedUserOperation types); sign with the account’s scheme. Put result in signature. 3. Send: Call EntryPoint’s handleOps([userOp], beneficiary).
Gas: verificationGasLimit covers validation and paymaster; callGasLimit covers execution; unused gas above a threshold is penalized. Use a bundler for estimation and ordering when possible.
Key points
- Always initialize the account (set signer) when using a factory; otherwise the account has no key.
- Use ERC-7739 (or equivalent) so signatures are bound to account and chain.
- For EOA delegation to an Account, use SignerEIP7702 and the EOA as sender; see EOA delegation reference.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/accounts.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/account-abstraction.adoc
-->
EOA Delegation (EIP-7702)
EIP-7702 lets an EOA delegate execution to a smart contract while keeping its private key. The EOA signs as usual; execution runs in the contract’s code. Use for batching (e.g. approve + transfer), sponsored txs, or limited-purpose keys. OpenZeppelin supports this via SignerEIP7702, which validates that the signer is the EOA (address(this)).
Delegation flow
1. Authorization: EOA signs an authorization message containing chain ID, nonce, delegation contract address, and signature fields. This restricts execution to that contract and prevents replay. Build the authorization with the wallet/RPC (e.g. viem signAuthorization with contractAddress and account or executor: "self"). 2. Set code: Send a transaction with type SET_CODE_TX_TYPE (0x04), authorizationList: [authorization], and data as the calldata to run in the EOA’s context. The EVM writes the delegation designator (0xef0100 || delegateAddress) to the EOA’s code so future calls to the EOA run the delegate contract’s code. 3. Execute: Subsequent calls to the EOA are handled by the delegate contract. To remove delegation, send a set-code tx with the authorization pointing to the zero address (clears code; does not clear EOA storage).
Account contract (SignerEIP7702)
Combine Account with SignerEIP7702 and (optionally) ERC7821 for batched execution. The account’s _rawSignatureValidation checks the EOA’s signature; sender in UserOps is the EOA address. No factory needed: the “account” is the EOA once delegated.
Using with ERC-4337
With the EOA delegated to an Account + SignerEIP7702, send UserOps with sender: eoa.address and initCode: "0x". Sign the UserOp hash with the EOA. When calling the EntryPoint, include the same authorization in the transaction (e.g. authorizationList: [authorization]) so the EOA is still delegated when the EntryPoint runs. Relayers should be aware that the EOA can invalidate authorization or move assets and leave the relayer unpaid.
Key points
- Delegate contracts must use replay-safe signatures (e.g. domain separator, nonce). A bad delegate can give an attacker control of the EOA.
- When changing delegation, use namespaced storage (e.g. ERC-7201) and treat it like an upgrade to avoid storage collisions; changing designator can make the EOA unusable if storage conflicts.
- Clearing delegation (zero address) resets code hash but does not clear EOA storage.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/eoa-delegation.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/accounts.adoc
-->
Governance (Governor)
On-chain governance: token holders propose and vote on actions; approved proposals are executed (optionally via a timelock). Build a Governor by composing base Governor with extensions for votes, quorum, counting, and optional timelock.
Components
- Voting power:
GovernorVoteshooks to anIVotestoken (e.g.ERC20Votes). Power is taken at the snapshot when the proposal becomes active (prevents double voting). UseERC20Votesfor ERC-20; for existing tokens without votes useERC20Wrapperto wrap 1:1 into a governance token. - Quorum: e.g.
GovernorVotesQuorumFraction(4)for 4% of supply at snapshot. - Counting: e.g.
GovernorCountingSimple— For, Against, Abstain; For and Abstain count toward quorum. - Timelock (recommended):
GovernorTimelockControl+TimelockController. The timelock executes proposals; grant the Governor the Proposer role and (usually) give Executor to the zero address so anyone can execute after delay. Timelock should hold funds/ownership, not the Governor.
Proposal lifecycle
1. Propose: propose(targets[], values[], calldatas[], description). Proposal id = hash of (targets, values, calldatas, descriptionHash). Data is not stored on-chain (gas saving); use events to reconstruct. 2. Vote: When active, delegates call castVote(proposalId, support) (0 Against, 1 For, 2 Abstain with GovernorCountingSimple). Only delegates have voting power; token holders must delegate (e.g. to self). 3. Queue (if timelock): After success, queue(targets, values, calldatas, descriptionHash) queues in the timelock. 4. Execute: After timelock delay (or immediately if no timelock), execute(...) runs the actions. With timelock, execution is via the timelock contract.
Set votingDelay, votingPeriod, and optionally proposalThreshold (e.g. in blocks or seconds; unit depends on token’s clock — see below).
Clock (block number vs timestamp)
From v4.9, voting uses IERC6372 clock. Default is block number. For timestamp-based governance (e.g. some L2s), override clock() and CLOCK_MODE() in the token (e.g. ERC20Votes) and set votingDelay/votingPeriod in time units; the Governor picks up the token’s clock. Old Governors are not compatible with new timestamp-based tokens.
Compatibility
- GovernorStorage: adds enumerable proposals and overloads that take only
proposalIdfor queue/execute/cancel (more calldata-efficient, more storage). - GovernorTimelockCompound: use when the timelock is Compound’s Timelock instead of OpenZeppelin’s
TimelockController. - ERC20VotesComp / Governor Bravo compatibility: use Comp variant for supply cap and Bravo-style interfaces if integrating with existing systems.
Key points
- Governor is modular; combine only the extensions you need. Use timelock and give it Proposer role; keep Executor/Canceller roles minimal.
- Proposal parameters must be passed again for queue/execute (not stored); get them from proposal creation events.
- Ensure token and Governor use the same clock (block vs timestamp) and same unit for delays/periods.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/governance.adoc
-->
Multisig
Multi-signature accounts require multiple signers to approve operations. OpenZeppelin supports this via ERC-7913 signers: SignerERC7913 (single signer), MultiSignerERC7913 (threshold), and MultiSignerERC7913Weighted (weighted threshold). Use with the Account (ERC-4337) contract for smart account multisig.
Single signer (ERC-7913)
SignerERC7913: one signer represented as bytes = verifier || key. Use for keys without an EVM address (e.g. hardware). Initialize the account with _setSigner(signer); expose setSigner with onlyEntryPointOrSelf so the account or entry point can rotate the key. Do not leave the account uninitialized (no public key).
Threshold multisig (MultiSignerERC7913)
Multiple signers, fixed threshold (e.g. 2-of-3). Initialize with _addSigners(signers) and _setThreshold(threshold). Public management: addSigners, removeSigners, setThreshold (guard with onlyEntryPointOrSelf). Contract ensures threshold is reachable (e.g. threshold ≤ number of signers). Query: isSigner(signer), getSigners(start, end), getSignerCount().
Weighted multisig (MultiSignerERC7913Weighted)
Like MultiSignerERC7913 but each signer has a weight; total weight of signing participants must meet or exceed the threshold. Initialize with _addSigners(signers), _setSignerWeights(signers, weights), _setThreshold(threshold). Use when signers have different authority (e.g. board votes, social recovery). Threshold scale must match weights (e.g. weights 1,2,3 → threshold 4 means at least two signers). _validateReachableThreshold() ensures sum of weights ≥ threshold.
Signature format
Multisig signature is abi.encode(signers[], signatures[]). signers must be sorted ascending by keccak256(signer); signatures in the same order. Each signer uses ERC-7913 format (verifier + key); each signature is the signer’s own signature.
Setup example (threshold)
bytes[] memory signers = new bytes[](3);
signers[0] = ecdsaSigner; // e.g. 20-byte EOA
signers[1] = abi.encodePacked(p256Verifier, pubKeyX, pubKeyY);
signers[2] = abi.encodePacked(rsaVerifier, abi.encode(rsaE, rsaN));
uint256 threshold = 2;
account.initialize(signers, threshold);For weighted: initialize(signers, weights, threshold) and ensure threshold is achievable from the sum of weights.
Key points
- Standard EIP-1271 assumes a single identity; ERC-7913 allows multiple signers and threshold/weighted rules.
- Use with
Account+ EIP712 + ERC7739 + ERC7821 (and token holders if the account holds NFTs/ERC1155). Restrict signer management toonlyEntryPointOrSelf. - Any custom logic on top of multisigner contracts must keep the threshold reachable (e.g. after removing signers).
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/multisig.adoc
- sources/openzeppelin/docs/modules/ROOT/pages/accounts.adoc
-->
Using with Upgrades
For upgradeable deployments (e.g. OpenZeppelin Upgrades Plugins), use @openzeppelin/contracts-upgradeable (peer: @openzeppelin/contracts). Same structure as main package with Upgradeable suffix and initializers instead of constructors.
Usage
import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol";
contract MyCollectible is ERC721Upgradeable {
function initialize() initializer public {
__ERC721_init("MyCollectible", "MCO");
}
}- No constructors; use internal
__{ContractName}_initand expose a publicinitialize()withinitializermodifier so it runs once. - With multiple inheritance, avoid double-init: use
__{ContractName}_init_unchainedonly when you have already run the full init for that contract elsewhere (manual and error-prone; prefer single linear init where possible).
Namespaced Storage (ERC-7201)
- Upgradeable contracts use namespaced storage (
@custom:storage-location erc7201:<NAMESPACE_ID>) so adding state or reordering inheritance does not shift storage and break upgrades. - Do not add non-namespaced state in the middle of inheritance; use the same pattern when extending.
Key Points
- Always call parent initializers in your public initializer; order matters for linearization.
- Storage layout must remain compatible across upgrades; use Upgrades Plugins/CLI to check.
- Interfaces and libraries are imported from main
@openzeppelin/contracts, not the upgradeable package.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/upgradeable.adoc
-->
Utilities
Libraries and contracts in @openzeppelin/contracts/utils and token/governance modules. Use via using X for type or inheritance as appropriate.
Cryptography
- ECDSA:
ECDSA.recover(hash, signature); useMessageHashUtils.toEthSignedMessageHash(hash)for Ethereum signed messages. Use for EOA signature verification. - SignatureChecker: Unified check for EOA (ECDSA), ERC-1271 (contract wallets), and ERC-7913.
SignatureChecker.isValidSignatureNow(signer, hash, signature). - MerkleProof:
MerkleProof.verify(proof, root, leaf)andmultiProofVerifyfor whitelists/airdrops. Build trees off-chain (e.g. OpenZeppelin merkle-tree JS). - P256/RSA: Use when you need non-ECDSA curves or RSA; see docs for verify interfaces.
Introspection
- ERC165 / IERC165:
supportsInterface(interfaceId). Implement withERC165and_registerInterface. UseERC165Checkerfor address:using ERC165Checker for address; token.supportsInterface(interfaceId).
Math
- Math / SignedMath:
using Math for uint256;thena.tryAdd(b),a.average(b), etc. Use for safe arithmetic and averages. - SafeCast: Safe casting with overflow checks when converting between integer types.
Structures
- EnumerableSet / EnumerableMap: Set/map with enumeration (e.g. iterate role members).
- Checkpoints: Time-indexed values for voting or history.
- MerkleTree (on-chain): Build and update Merkle roots on-chain; use custom hash consistently.
- BitMaps, DoubleEndedQueue, Heap: Packed booleans, queue, priority queue; see API.
Storage and Low-Level
- StorageSlot:
StorageSlot.getAddressSlot(slot).valuefor proxy/implementation slots. Use for ERC-1967 or custom slots; avoid collision with Solidity layout. - SlotDerivation: ERC-7201 namespaced slot:
bytes32 namespace; namespace.erc7201Slot(). - TransientSlot: Transient storage (EIP-1153) via UDVTs.
- LowLevelCall:
target.callNoReturn(data)orcallReturn64Bytes(data)to cap return size and avoid return bombing.
Misc
- Base64: Encode bytes for Data URI (e.g. tokenURI).
- Multicall: Inherit
Multicall; callmulticall(data[])to batch calls and revert all if one fails. - Time:
Time.Delayfor safe, setback-resistant delay updates (e.g. governance).Blockhashfor L2 historical block hashes (EIP-2935).
Key Points
- Prefer SignatureChecker when accepting both EOA and contract signatures.
- Use MerkleProof for gas-efficient whitelists; keep tree building off-chain.
- StorageSlot/SlotDerivation are for advanced patterns (proxies, namespaced storage); ensure no slot collision.
<!-- Source references:
- sources/openzeppelin/docs/modules/ROOT/pages/utilities.adoc
-->