OpenZeppelin Contracts
OpenZeppelin Contracts is the standard library for secure smart contract development. Used by Uniswap, Aave, Compound, and most production Solidity projects. The library provides audited, modular building blocks for tokens, access control, security patterns, upgradeability, and governance.
What You Probably Got Wrong
OZ v5 shipped breaking changes in late 2023. Most LLM training data reflects v4. Every item below will cause compilation failures if you use the old pattern.
- Ownable requires a constructor argument --
Ownableno longer has a default owner. You MUST pass the initial owner address:Ownable(initialOwner). Omitting this is the #1 compilation error. - Import paths changed -- v5 uses
@openzeppelin/contracts/token/ERC20/ERC20.sol, not@openzeppelin/contracts/ERC20.sol. Some nested paths also changed. Always import from the full canonical path. _setupRoleis removed -- Use_grantRole(role, account)in the constructor instead._setupRoledoes not exist in v5._safeMintinternal visibility --_safeMintis still internal but the override pattern changed. Override_updateinstead of_beforeTokenTransfer/_afterTokenTransfer.- Hook functions renamed --
_beforeTokenTransferand_afterTokenTransferare gone. Override_update(from, to, value)for ERC-20 and_update(to, tokenId, auth)for ERC-721. - ERC-721 Enumerable override -- v5 requires overriding
_increaseBalancein addition to_updatewhen combining ERC721Enumerable with other extensions. SafeMathis removed -- Solidity 0.8+ has built-in overflow checks.SafeMathwas deleted from v5 entirely.Countersis removed -- Use plainuint256with++instead. TheCounterslibrary was removed for being unnecessary abstraction.AccessControl.renounceRolerequirescallerConfirmation-- In v5,renounceRole(role, callerConfirmation)takes a second parameter to prevent accidental renouncement.
Quick Start
Installation
# Foundry
forge install OpenZeppelin/openzeppelin-contracts
# Hardhat / npm
npm install @openzeppelin/contracts
# Upgradeable variant
npm install @openzeppelin/contracts-upgradeable
Foundry Remappings
# remappings.txt
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
Minimum Viable Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1_000_000 * 10 ** decimals());
}
}
Token Standards
ERC-20 with Extensions
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Burnable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
/// @notice Governance token with gasless approvals (EIP-2612) and vote delegation (EIP-5805).
contract GovernanceToken is ERC20, ERC20Burnable, ERC20Permit, ERC20Votes {
constructor(address initialHolder)
ERC20("GovernanceToken", "GOV")
ERC20Permit("GovernanceToken")
{
_mint(initialHolder, 10_000_000 * 10 ** decimals());
}
// v5 override resolution: ERC20Votes needs _update, Nonces is shared between Permit and Votes
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);
}
}
ERC-721 with Extensions
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract GameNFT is ERC721, ERC721Enumerable, ERC721URIStorage, Ownable {
uint256 private _nextTokenId;
constructor(address initialOwner)
ERC721("GameNFT", "GNFT")
Ownable(initialOwner)
{}
function safeMint(address to, string calldata uri) external onlyOwner returns (uint256) {
uint256 tokenId = _nextTokenId++;
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
return tokenId;
}
// v5 requires both _update and _increaseBalance overrides for Enumerable
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 tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable, ERC721URIStorage)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
ERC-1155 Multi-Token
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC1155} from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract GameItems is ERC1155, Ownable {
uint256 public constant GOLD = 0;
uint256 public constant SILVER = 1;
uint256 public constant SWORD = 2;
constructor(address initialOwner)
ERC1155("https://game.example/api/item/{id}.json")
Ownable(initialOwner)
{
_mint(initialOwner, GOLD, 10 ** 18, "");
_mint(initialOwner, SILVER, 10 ** 27, "");
_mint(initialOwner, SWORD, 1, "");
}
function mint(address to, uint256 id, uint256 amount, bytes calldata data) external onlyOwner {
_mint(to, id, amount, data);
}
function mintBatch(address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data)
external
onlyOwner
{
_mintBatch(to, ids, amounts, data);
}
}
Access Control
Ownable (Single Admin)
Simplest model: one address has admin privileges. Good for token contracts and simple protocols.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract Vault is Ownable {
// v5: MUST pass initial owner to constructor
constructor(address initialOwner) Ownable(initialOwner) {}
function withdraw() external onlyOwner {
payable(owner()).transfer(address(this).balance);
}
// Two-step ownership transfer (built into v5 via Ownable2Step)
}
For two-step transfers (prevents transferring to a wrong address):
import {Ownable2Step, Ownable} from "@openzeppelin/contracts/access/Ownable2Step.sol";
contract SafeVault is Ownable2Step {
constructor(address initialOwner) Ownable(initialOwner) {}
}
// New owner must call acceptOwnership() to complete the transfer
AccessControl (Role-Based)
Multiple roles with independent permissions. Each role has an admin role that can grant/revoke it.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
contract Treasury is AccessControl {
bytes32 public constant TREASURER_ROLE = keccak256("TREASURER_ROLE");
bytes32 public constant AUDITOR_ROLE = keccak256("AUDITOR_ROLE");
event Withdrawal(address indexed to, uint256 amount);
constructor(address admin) {
// v5: use _grantRole in constructor (not _setupRole, which is removed)
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(TREASURER_ROLE, admin);
}
function withdraw(address payable to, uint256 amount) external onlyRole(TREASURER_ROLE) {
emit Withdrawal(to, amount);
to.transfer(amount);
}
// v5: renounceRole requires callerConfirmation to prevent accidents
// renounceRole(TREASURER_ROLE, msg.sender)
}
AccessManager (v5 Centralized Permission Hub)
New in v5. A single contract that manages permissions across your entire protocol. Contracts delegate their access checks to the AccessManager rather than storing roles internally.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {AccessManaged} from "@openzeppelin/contracts/access/manager/AccessManaged.sol";
/// @notice Any contract that delegates auth to AccessManager.
contract ManagedVault is AccessManaged {
constructor(address manager) AccessManaged(manager) {}
// restricted modifier checks with the AccessManager
function withdraw() external restricted {
payable(msg.sender).transfer(address(this).balance);
}
}
AccessManager setup (done off-chain or in a deploy script):
import {AccessManager} from "@openzeppelin/contracts/access/manager/AccessManager.sol";
// In deploy script:
// 1. Deploy AccessManager with initial admin
// AccessManager manager = new AccessManager(adminAddress);
//
// 2. Label a role
// uint64 WITHDRAWER_ROLE = 1; // role IDs are uint64, 0 is ADMIN_ROLE
//
// 3. Grant role to an address
// manager.grantRole(WITHDRAWER_ROLE, withdrawerAddress, 0); // 0 = no grant delay
//
// 4. Set which role can call which function on the target
// bytes4[] memory selectors = new bytes4[](1);
// selectors[0] = ManagedVault.withdraw.selector;
// manager.setTargetFunctionRole(address(vault), selectors, WITHDRAWER_ROLE);
Security Utilities
ReentrancyGuard
Prevents reentrant calls to state-changing functions. Apply to any function that makes external calls after state changes.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract LendingPool is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external nonReentrant {
// CEI: Checks-Effects-Interactions
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
SafeERC20
Wraps ERC-20 calls to handle tokens that don't return bool (like USDT) and tokens that return false on failure.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract PaymentSplitter {
using SafeERC20 for IERC20;
function collectPayment(IERC20 token, address from, uint256 amount) external {
// safeTransferFrom reverts on failure and handles non-standard return values
token.safeTransferFrom(from, address(this), amount);
}
function distribute(IERC20 token, address to, uint256 amount) external {
token.safeTransfer(to, amount);
}
function approveSpender(IERC20 token, address spender, uint256 amount) external {
// forceApprove handles tokens that require approval to be set to 0 first (USDT)
token.forceApprove(spender, amount);
}
}
Pausable
Emergency stop mechanism. Commonly used to freeze a protocol during an exploit.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract Exchange is Ownable, Pausable {
constructor(address initialOwner) Ownable(initialOwner) {}
function swap(address tokenIn, address tokenOut, uint256 amount) external whenNotPaused {
// swap logic only executes when not paused
}
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
}
Address Utilities
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
contract Sender {
using Address for address payable;
function sendETH(address payable to, uint256 amount) external {
// sendValue reverts if the transfer fails (unlike .transfer which has 2300 gas limit issues)
to.sendValue(amount);
}
}
Upgradeable Contracts
UUPS Pattern
UUPS (Universal Upgradeable Proxy Standard, EIP-1822) places the upgrade logic in the implementation contract. This is the recommended pattern in OZ v5 -- TransparentProxy is still available but UUPS is lighter on gas.
Install the upgradeable package:
npm install @openzeppelin/contracts-upgradeable
Implementation contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
contract VaultV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 public totalDeposits;
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address initialOwner) public initializer {
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
}
function deposit() external payable {
totalDeposits += msg.value;
}
// Only owner can authorize upgrades
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
Deploy with Foundry:
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
// In deploy script:
// 1. Deploy implementation
// VaultV1 impl = new VaultV1();
//
// 2. Encode initializer call
// bytes memory data = abi.encodeCall(VaultV1.initialize, (adminAddress));
//
// 3. Deploy proxy pointing to implementation
// ERC1967Proxy proxy = new ERC1967Proxy(address(impl), data);
//
// 4. Interact through proxy
// VaultV1 vault = VaultV1(address(proxy));
Upgrade to V2:
contract VaultV2 is Initializable, UUPSUpgradeable, OwnableUpgradeable {
uint256 public totalDeposits;
uint256 public withdrawalFee; // New state variable -- append only, never reorder
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address initialOwner) public initializer {
__Ownable_init(initialOwner);
__UUPSUpgradeable_init();
}
// New initializer for the upgrade (reinitializer(2) for second version)
function initializeV2(uint256 fee) public reinitializer(2) {
withdrawalFee = fee;
}
function deposit() external payable {
totalDeposits += msg.value;
}
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}
// Upgrade call:
// VaultV1(proxy).upgradeToAndCall(address(newImpl), abi.encodeCall(VaultV2.initializeV2, (100)));
Upgrade Safety Rules
- Never reorder or remove existing state variables -- only append new ones at the end.
- Use
_disableInitializers()in the constructor of every implementation contract. - Use
reinitializer(n)for version-specific init -- notinitializer(which can only run once). - No immutable variables that depend on constructor args in upgradeable contracts (the proxy won't have them).
- Storage gaps -- v5 uses namespaced storage (ERC-7201) instead of
__gaparrays. If extending OZ upgradeable contracts, the gaps are handled internally.
Defender Integration
OpenZeppelin Defender provides operational security for deployed contracts: monitoring, automated actions, and secure transaction execution.
Key Components
| Component | Purpose |
|---|---|
| Monitor | Watch on-chain events and trigger alerts |
| Actions | Serverless functions that run in response to triggers |
| Relayers | Managed EOAs for sending transactions (no key management) |
| Transaction Proposals | Multisig-compatible tx submission (Safe, Governor) |
| Deploy | Deploy and verify contracts with upgrade safety checks |
Deploy + Upgrade via Defender
npm install @openzeppelin/defender-sdk
import { Defender } from "@openzeppelin/defender-sdk";
const client = new Defender({
apiKey: process.env.DEFENDER_API_KEY,
apiSecret: process.env.DEFENDER_API_SECRET,
});
// Propose an upgrade through a multisig
const proposal = await client.upgrade.proposeUpgrade({
contract: { address: "0xPROXY", network: "mainnet" },
newImplementation: "0xNEW_IMPL",
via: "0xMULTISIG", // Safe address
viaType: "Gnosis Safe",
});
Monitor Setup
const monitor = await client.monitor.create({
type: "BLOCK",
name: "Large Transfers",
network: "mainnet",
addresses: ["0xTOKEN_ADDRESS"],
abi: '[{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}]',
eventConditions: [{ eventSignature: "Transfer(address,address,uint256)", expression: "value > 1000000000000000000000" }],
alertThreshold: { amount: 1, windowSeconds: 3600 },
notificationChannels: ["email-channel-id"],
});
Common Patterns
Combining Extensions
When multiple OZ contracts override the same function, you must resolve the diamond. The pattern is always the same: override the conflicting function and call super.
// Multiple inheritance resolution example
function _update(address from, address to, uint256 value)
internal
override(ERC20, ERC20Votes, ERC20Pausable)
{
super._update(from, to, value);
}
The super call invokes the rightmost parent first (C3 linearization). Order your inheritance from most base to most derived:
// Correct: base -> extensions
contract Token is ERC20, ERC20Burnable, ERC20Pausable, ERC20Votes { ... }
Governor (On-Chain Governance)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Governor} from "@openzeppelin/contracts/governance/Governor.sol";
import {GovernorCountingSimple} from "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import {GovernorVotes} from "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import {GovernorVotesQuorumFraction} from "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import {GovernorTimelockControl} from "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
contract MyGovernor is
Governor,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
constructor(IVotes token, TimelockController timelock)
Governor("MyGovernor")
GovernorVotes(token)
GovernorVotesQuorumFraction(4) // 4% quorum
GovernorTimelockControl(timelock)
{}
function votingDelay() public pure override returns (uint256) {
return 7200; // ~1 day at 12s blocks
}
function votingPeriod() public pure override returns (uint256) {
return 50400; // ~1 week at 12s blocks
}
function proposalThreshold() public pure override returns (uint256) {
return 100_000 * 10 ** 18; // 100k tokens to propose
}
// Required overrides for diamond resolution
function state(uint256 proposalId)
public
view
override(Governor, GovernorTimelockControl)
returns (ProposalState)
{
return super.state(proposalId);
}
function proposalNeedsQueuing(uint256 proposalId)
public
view
override(Governor, GovernorTimelockControl)
returns (bool)
{
return super.proposalNeedsQueuing(proposalId);
}
function _queueOperations(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint48) {
return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash);
}
function _executeOperations(
uint256 proposalId,
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) {
super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
}
function _cancel(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 descriptionHash
) internal override(Governor, GovernorTimelockControl) returns (uint256) {
return super._cancel(targets, values, calldatas, descriptionHash);
}
function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) {
return super._executor();
}
}
ERC-20 with Capped Supply and Pausability
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Capped} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Capped.sol";
import {ERC20Pausable} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
contract CappedToken is ERC20Capped, ERC20Pausable, Ownable {
constructor(address initialOwner)
ERC20("CappedToken", "CAP")
ERC20Capped(100_000_000 * 10 ** 18) // 100M cap
Ownable(initialOwner)
{
_mint(initialOwner, 10_000_000 * 10 ** 18);
}
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
function pause() external onlyOwner { _pause(); }
function unpause() external onlyOwner { _unpause(); }
function _update(address from, address to, uint256 value)
internal
override(ERC20Capped, ERC20Pausable)
{
super._update(from, to, value);
}
}
Error Handling
| Error | Cause | Fix |
|---|---|---|
OwnableUnauthorizedAccount(account) |
Caller is not the owner | Check owner() before calling onlyOwner functions |
OwnableInvalidOwner(owner) |
Trying to set owner to address(0) |
Pass a valid address to the Ownable constructor |
AccessControlUnauthorizedAccount(account, role) |
Caller lacks the required role | Grant role via grantRole(role, account) from the role's admin |
EnforcedPause() |
Function called while contract is paused | Call unpause() first |
ExpectedPause() |
unpause() called when not paused |
Check paused() state before calling |
ERC20InsufficientBalance(sender, balance, needed) |
Transfer amount exceeds balance | Verify balances before transfer |
ERC721NonexistentToken(tokenId) |
Querying a token that doesn't exist | Check _exists or catch the revert |
InvalidInitialization() |
initializer called more than once |
Use reinitializer(n) for subsequent inits |
Troubleshooting
"Identifier already declared" on Multiple Inheritance
Symptom: Compiler error when two parent contracts define the same function.
Fix: Add an explicit override that calls super:
function _update(...) internal override(ParentA, ParentB) {
super._update(...);
}
List ALL parents that define the function in the override(...) specifier.
Upgradeable Contract Won't Initialize
Symptom: InvalidInitialization() revert on proxy deployment.
Fix: Ensure the implementation constructor calls _disableInitializers(). The proxy calls initialize() -- if the implementation already initialized itself, the proxy call fails.
Storage Collision After Upgrade
Symptom: State variables return garbage after upgrading.
Fix: Never reorder, rename, or remove state variables. Only append new variables at the end. In v5, OZ uses ERC-7201 namespaced storage internally, but your custom variables must still follow append-only ordering.
Security Considerations
- Always use
SafeERC20when interacting with arbitrary ERC-20 tokens -- many don't follow the standard return value convention. - Apply
ReentrancyGuardto functions that transfer ETH or call external contracts after state changes, even with CEI pattern as defense-in-depth. - Use
Ownable2StepoverOwnablefor any contract where losing ownership is catastrophic. The two-step process prevents transferring to a typo address. - For upgradeable contracts, use the
@openzeppelin/upgradesHardhat/Foundry plugin to validate storage layout compatibility before deploying upgrades. - Never leave
_authorizeUpgradeunprotected. Always gate it withonlyOwneror a role check.
References
- OpenZeppelin Contracts v5 Docs
- GitHub: openzeppelin-contracts
- OpenZeppelin Contracts Wizard -- generates contract code with selected extensions
- v5 Migration Guide
- OpenZeppelin Defender
- ERC-7201: Namespaced Storage Layout
- EIP-1822: UUPS
- EIP-2612: Permit