ERC-20 Token with OpenZeppelin: The Complete Technical Guide
Every serious ERC-20 token deployed in the last several years has one thing in common: it either uses OpenZeppelin contracts directly or borrows heavily from them. OpenZeppelin isn't just a code library — it's the security foundation that the entire Ethereum token ecosystem is built on. If you want to create an ERC-20 token the right way, you need to understand what OpenZeppelin actually does, why its contract implementations are trusted with billions of dollars in value, and how to use it effectively.
This guide goes deeper than most. We're not just going to tell you to import a library and call it a day. We're going to walk through the actual OpenZeppelin ERC-20 implementation, explain every function, show you what the extensions do, and explain why rolling your own ERC-20 from scratch is almost always the wrong choice — even for experienced Solidity developers.
Whether you want to create erc20 token with OpenZeppelin through a no-code interface or you're a developer building on top of the standard directly, this is the reference you need.
What Is OpenZeppelin?
OpenZeppelin is an open-source framework for developing secure smart contracts on Ethereum and other EVM-compatible blockchains. It was founded in 2016 by Demian Brener and Manuel Aráoz, originally under the name Zeppelin Solutions, and has since grown into arguably the most important piece of shared infrastructure in the Ethereum developer ecosystem.
The core of what OpenZeppelin provides is a library of battle-tested, audited Solidity contracts that implement the most common token standards, access control patterns, security utilities, and governance mechanisms. Instead of building these primitives yourself — with all the attendant risk of subtle bugs, missed edge cases, and reentrancy vulnerabilities — you import OpenZeppelin's implementations and inherit from them.
The numbers are staggering. As of 2025, OpenZeppelin contracts protect over $10 billion in value across live deployments. The library has been downloaded hundreds of millions of times via npm. Every major DeFi protocol — Uniswap, Aave, Compound, Curve, MakerDAO — either uses OpenZeppelin contracts directly or has had its contracts audited against OpenZeppelin's security standards. If you're deploying anything on Ethereum today and you're not using OpenZeppelin, you owe the community a very good explanation of why.
OpenZeppelin also provides a security audit service (used by the biggest protocols in the space), the Defender platform for smart contract operations and monitoring, and extensive documentation and tooling. But for most developers and builders, the contracts library is what matters — and specifically the ERC-20 implementation, which is the subject of this guide.
The History and Trust Behind OpenZeppelin
To understand why OpenZeppelin has become the default choice for creating an ERC-20 token, you need to understand the context it emerged from. The early years of Ethereum smart contract development were marked by catastrophic, publicly visible security failures. The DAO hack of 2016 — where a reentrancy vulnerability allowed an attacker to drain $60 million worth of ETH — shook the entire ecosystem and led to the Ethereum hard fork that created today's Ethereum and Ethereum Classic.
That hack, and the dozens of smaller ones that followed, made it obvious that smart contract security wasn't something you could bolt on after the fact. The code is public, immutable once deployed, and manages real money. You have one shot to get it right. The cost of a bug is not a patch and a restart — it's potentially the entire balance of the contract, drained instantly by anyone who finds the exploit.
OpenZeppelin's founders saw this problem clearly and decided to solve it at the infrastructure level. Instead of every team independently trying to implement secure token contracts, access control, and pausing mechanisms, they would build these primitives once, audit them rigorously, make them public, and let the entire ecosystem benefit. The economic logic was compelling: a single well-funded team doing deep security work on shared code is far more effective than ten thousand teams each doing shallow security work on their own implementations.
The result is a library where every contract has been:
- Reviewed by multiple senior Solidity security researchers
- Formally audited and the audit reports published publicly
- Battle-tested in production across thousands of deployments handling billions of dollars
- Continuously updated to address newly discovered vulnerability classes
- Peer-reviewed by the global Solidity developer community via open-source contributions
The trust OpenZeppelin has earned is not theoretical — it's demonstrated by what the most sophisticated actors in the ecosystem choose to build on. When you see that Uniswap's governance token uses OpenZeppelin's ERC-20, that Compound's cTokens inherit from OpenZeppelin, and that MakerDAO's team references OpenZeppelin security patterns in their audit reports, that's not coincidence. Those teams evaluated every alternative and chose OpenZeppelin because it's the most secure foundation available.
The ERC-20 Standard Interface — What OpenZeppelin Implements
Before diving into OpenZeppelin's specific implementation, it's worth being precise about what the ERC-20 standard actually requires. EIP-20 defines a minimum interface that every conforming token contract must expose. OpenZeppelin implements this interface faithfully and then adds a carefully considered set of internal utilities on top.
The mandatory ERC-20 interface consists of six functions and two events:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
// Returns total token supply
function totalSupply() external view returns (uint256);
// Returns token balance of a given address
function balanceOf(address account) external view returns (uint256);
// Transfers tokens from caller to recipient
function transfer(address to, uint256 amount) external returns (bool);
// Returns how much spender can spend on owner's behalf
function allowance(address owner, address spender) external view returns (uint256);
// Approves spender to spend up to amount on caller's behalf
function approve(address spender, uint256 amount) external returns (bool);
// Transfers tokens from one address to another (requires approval)
function transferFrom(address from, address to, uint256 amount) external returns (bool);
// Emitted when tokens move between addresses
event Transfer(address indexed from, address indexed to, uint256 value);
// Emitted when an approval is set or updated
event Approval(address indexed owner, address indexed spender, uint256 value);
}
OpenZeppelin also implements the optional metadata extension — name(), symbol(), and decimals() — which virtually every real-world token needs. The decimals() function deserves special attention because it's a source of frequent confusion. ERC-20 tokens store balances as unsigned integers with no floating point. The decimals value tells applications how many decimal places to display. The default in OpenZeppelin is 18, matching ETH. So if your token has 18 decimals, a balance of 1000000000000000000 (10^18) represents exactly 1 token. A total supply of 1,000,000 tokens at 18 decimals means your totalSupply() returns 1000000000000000000000000.
Most tokens use 18 decimals by default, which is the same precision as ETH. If you're building a stablecoin or a token meant to map to a fiat currency, you might choose 6 decimals to match USDC and USDT. You can override decimals() in your contract to return any value between 0 and 18. Just make sure you account for this in your mint amounts — if you use 6 decimals and want 1,000,000 tokens, your mint amount should be 1000000 * 10**6.
Breaking Down the OpenZeppelin ERC-20 Contract
The OpenZeppelin ERC-20 implementation lives in @openzeppelin/contracts/token/ERC20/ERC20.sol. It's about 300 lines of Solidity including comments — remarkably compact for something that's this widely relied upon. Let's work through the key pieces.
The state variables are minimal by design:
mapping(address account => uint256) private _balances;
mapping(address account => mapping(address spender => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
That's it. Five storage variables cover everything the contract needs to track. The _balances mapping associates each address with its token balance. The _allowances mapping is a two-dimensional structure tracking how much each spender is approved to spend from each owner's balance. Both are private, meaning external contracts cannot directly read them — they must use the public getter functions, which enforces the proper interface.
The constructor is intentionally simple:
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
There's no minting in the constructor by default. Token distribution is handled separately through the _mint internal function. This separation of concerns is intentional and important — it lets you control exactly when and how tokens enter circulation, without being constrained by what the constructor can do.
The transfer function illustrates OpenZeppelin's approach to safe token movement:
function transfer(address to, uint256 value) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
It delegates to the internal _transfer function, which is where the actual logic and validation lives. This pattern — a thin public function that delegates to an internal function with the real logic — appears throughout OpenZeppelin's codebase. It makes the contracts easier to customize because you can override the internal function without reimplementing the public interface.
The internal _transfer function performs the critical checks:
function _transfer(address from, address to, uint256 value) internal {
if (from == address(0)) {
revert ERC20InvalidSender(address(0));
}
if (to == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(from, to, value);
}
Notice the zero-address checks. Sending tokens to address(0) means burning them irrecoverably — they're gone forever. OpenZeppelin prevents this from happening accidentally through transfer (though it allows it explicitly through _burn). The actual balance update happens in _update, which is the lowest-level function in the hierarchy and the one that emits the Transfer event.
The _mint and _burn Internal Functions
Two of the most important functions in any ERC-20 implementation are _mint and _burn. OpenZeppelin marks both as internal, meaning they can only be called from within the contract or by inheriting contracts — not from external addresses. This is a deliberate security design choice.
_mint creates new tokens and assigns them to an address:
function _mint(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidReceiver(address(0));
}
_update(address(0), account, value);
}
When _update is called with address(0) as the sender, it increases _totalSupply by the minted amount and increases the recipient's balance. It also emits a Transfer event with from set to address(0) — this is the conventional signal to blockchain indexers that tokens were minted rather than transferred from an existing holder.
_burn is the mirror image — it destroys tokens:
function _burn(address account, uint256 value) internal {
if (account == address(0)) {
revert ERC20InvalidSender(address(0));
}
_update(account, address(0), value);
}
When called with address(0) as the recipient, _update decreases both the account's balance and the _totalSupply. A Transfer event with to set to address(0) signals a burn to indexers.
The critical design point: because _mint and _burn are internal, you as the contract author must decide who can call them. OpenZeppelin gives you the building blocks but doesn't make the access control decision for you. That's where extensions like Ownable come in.
A common mistake when creating an ERC-20 token is accidentally making _mint callable by anyone. If you expose a public mint function without proper access control, any address can mint unlimited tokens, instantly destroying your token's value. Always pair any public minting function with a role check — either onlyOwner from Ownable, or a role from AccessControl.
Access Control — Ownable and Roles
OpenZeppelin provides two main access control patterns that you'll use when building on top of ERC-20: Ownable and AccessControl.
Ownable is the simpler pattern. It designates a single address as the "owner" of the contract and provides a modifier — onlyOwner — that restricts function access to that address. Here's what a minimal ownable mintable token looks like:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor(address initialOwner)
ERC20("My Token", "MTK")
Ownable(initialOwner)
{
_mint(initialOwner, 1_000_000 * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
With this pattern, only the address passed as initialOwner at deployment can call mint. If your token needs to be managed by a team or a multisig wallet (which you should strongly consider for any serious project), you'd pass a Gnosis Safe address as the owner rather than an EOA (externally owned account).
Ownership can be transferred with transferOwnership(address newOwner) or renounced entirely with renounceOwnership(). Renouncing ownership permanently removes any owner-gated capabilities — useful if you want to make your token immutable and trustless after the initial setup period.
AccessControl is OpenZeppelin's more sophisticated pattern for projects that need multiple roles with different permissions. It lets you define arbitrary roles (represented as bytes32 hashes), assign those roles to specific addresses, and check for role membership in your functions:
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MyToken is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
constructor() ERC20("My Token", "MTK") {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}
This pattern is ideal for protocols where minting rights need to be granted to smart contracts (like a staking contract that mints rewards) while keeping administrative control with a multisig. For a thorough treatment of all the security considerations involved, the ERC-20 smart contract security guide covers access control in depth alongside other critical vulnerability classes.
Optional Extensions: Mintable, Burnable, Pausable, Permit
One of OpenZeppelin's most valuable design choices is modular extensions. The base ERC-20 contract is intentionally minimal — it just implements the standard. Additional behaviors are added through separate extension contracts that you mix in using Solidity's multiple inheritance.
ERC20Burnable adds a public burn function that lets token holders burn their own tokens, plus a burnFrom function that lets an approved spender burn tokens on the holder's behalf. It inherits from ERC-20 and simply exposes _burn in a controlled way:
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
contract MyToken is ERC20, ERC20Burnable {
// Holders can now call burn(amount) and burnFrom(address, amount)
}
ERC20Pausable adds the ability to pause all token transfers globally. When paused, every call to transfer, transferFrom, mint, and burn reverts. This is a circuit-breaker — useful if a security incident is detected and you need to freeze token movements while you assess the situation. You combine it with Ownable or AccessControl to control who can pause and unpause:
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
contract MyToken is ERC20, ERC20Pausable, Ownable {
function pause() public onlyOwner { _pause(); }
function unpause() public onlyOwner { _unpause(); }
// Required override when combining ERC20 and ERC20Pausable
function _update(address from, address to, uint256 value)
internal override(ERC20, ERC20Pausable) {
super._update(from, to, value);
}
}
Pausable tokens introduce a significant trust assumption — the entity that can pause can freeze everyone's tokens. For any token with meaningful value, the pause capability should be controlled by a multisig with a reasonable threshold (e.g., 3-of-5 signers) rather than a single EOA. Listing on exchanges and DeFi protocols may also be complicated by the presence of a pause function, as it represents centralization risk.
ERC20Permit implements EIP-2612, which allows approvals to be set via off-chain signatures rather than on-chain transactions. This solves the UX problem where users have to send an "approve" transaction before they can interact with a DeFi protocol. With Permit, you sign a message off-chain, and the DeFi contract submits the approval and the action in a single transaction — saving a transaction and making the experience much smoother. Uniswap uses this extensively in their interface.
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
contract MyToken is ERC20, ERC20Permit {
constructor() ERC20("My Token", "MTK") ERC20Permit("My Token") {}
}
ERC20Votes adds on-chain governance support, enabling your token to be used for voting in OpenZeppelin Governor-based DAOs. It tracks delegation and voting power checkpoints, making it possible to query historical voting power at any block — essential for governance systems that need to prevent vote manipulation through last-minute token purchases.
For a well-structured token economy that uses these extensions effectively, the ERC-20 tokenomics design guide walks through how supply mechanics, burn mechanisms, and governance rights interact in real-world token deployments.
How OpenZeppelin Prevents Common Vulnerabilities
Understanding what OpenZeppelin protects against helps you appreciate why it's worth using. Here are the most significant vulnerability classes the library addresses.
Integer overflow and underflow — Before Solidity 0.8, arithmetic operations on unsigned integers could silently wrap around. Subtracting 1 from a uint256 with value 0 would give you 2^256-1 instead of reverting. This class of bug has been exploited multiple times in the wild. Solidity 0.8+ made checked arithmetic the default, and OpenZeppelin contracts require Solidity 0.8+. If you're using an older compiler version with an older OpenZeppelin version, the library used SafeMath explicitly for all arithmetic operations.
Reentrancy — The most famous Ethereum vulnerability class. OpenZeppelin's ERC-20 implementation is not vulnerable to reentrancy because it uses the checks-effects-interactions pattern and doesn't make external calls in its core transfer logic. The _update function updates all state before emitting events, and no external contracts are called during token transfers. OpenZeppelin also provides a ReentrancyGuard contract for functions in your contracts that do make external calls.
Allowance front-running (the ERC-20 approve race) — A known quirk of the original ERC-20 approve mechanism is that changing an allowance from a non-zero value to another non-zero value creates a window where a malicious spender can spend the old allowance before the new one is set. OpenZeppelin's implementation includes increaseAllowance and decreaseAllowance helper functions to safely adjust allowances, and newer versions implement approve with the understanding that applications should set allowances to zero before setting them to a new non-zero value.
Zero-address transfers — Accidentally sending tokens to address(0) burns them irrecoverably. OpenZeppelin's _transfer explicitly reverts with ERC20InvalidReceiver(address(0)) if you try this, protecting users from costly mistakes.
Inconsistent event emission — Blockchain indexers, explorers, and wallets rely on the Transfer and Approval events to track state. OpenZeppelin guarantees these events are always emitted correctly and consistently, even for mints (Transfer from address(0)) and burns (Transfer to address(0)).
How ERC Token Creator Uses OpenZeppelin Under the Hood
When you use the ERC-20 token creator at erc20token.app to deploy a token, the contract that gets deployed to the blockchain is built on OpenZeppelin. This isn't just marketing — it's the architecture that makes the platform trustworthy for real deployments.
The platform selects the appropriate OpenZeppelin extensions based on the features you configure. If you enable minting after deployment, the contract inherits from ERC-20 and uses the Ownable pattern to gate the mint function. If you enable burning, ERC20Burnable is mixed in. If you configure a pause capability, ERC20Pausable is added with appropriate access control. The initial supply is minted in the constructor using _mint, sending all tokens directly to your deployer address.
The result is a contract that is semantically identical to what an experienced Solidity developer would write manually — same OpenZeppelin base, same extension mix, same security properties — but generated from your configuration without you having to write a line of code. You can create an ERC-20 token with enterprise-grade security without needing to understand the Solidity internals, though understanding them (as you're doing right now) is always valuable context.
After deployment, you can verify your contract on Etherscan, which makes the source code publicly readable. Because the deployed contract is built on OpenZeppelin — whose source code is extensively documented and publicly available — verification is straightforward and gives your token holders full transparency into what rules govern their tokens.
The broader advantage is that using a no-code tool backed by OpenZeppelin gives you the same security foundation as a custom deployment, plus the ability to get from decision to live contract in minutes rather than days. The step-by-step token creation guide walks through the full process of deploying through ERC Token Creator, from wallet connection through post-deployment verification.
Comparing OpenZeppelin vs. Custom Solidity — Why OZ Wins
The question of whether to use OpenZeppelin or write a custom ERC-20 implementation comes up regularly, especially among developers who feel confident in their Solidity skills. The answer is almost always: use OpenZeppelin. Here's why.
Auditing economics — Getting a smart contract security audit from a reputable firm costs anywhere from $15,000 to $150,000 depending on scope and the firm's reputation. OpenZeppelin's contracts have already been audited by multiple firms, with reports published publicly. When you inherit from OpenZeppelin, you inherit the audit coverage. You still need to audit your own additions and customizations, but the foundation is already covered.
Battle testing — OpenZeppelin ERC-20 contracts have been running in production, handling hundreds of billions of dollars in value, for years. Every edge case that can be found in normal production usage has been found. Your custom implementation hasn't been through that gauntlet. The bugs that will kill your project won't be in obvious places — they'll be in the subtle interactions between functions that only reveal themselves under specific conditions.
Developer velocity — Writing a compliant, secure ERC-20 from scratch takes a skilled Solidity developer several days of careful work, followed by testing and auditing. Importing OpenZeppelin and writing a 20-line contract that inherits the standard takes an afternoon. Unless you have a genuinely compelling reason to deviate from the standard implementation, that time delta is pure waste.
Ecosystem compatibility — OpenZeppelin's implementation matches exactly what DeFi protocols expect. If you write a custom implementation with slightly different behavior — even if technically ERC-20 compliant — you may encounter integration issues with protocols that have made assumptions based on OpenZeppelin's behavior. Fee-on-transfer tokens and rebasing tokens are famous examples of tokens that break many DeFi protocol integrations because they deviate from expected behavior.
Upgradability — OpenZeppelin also provides transparent proxy and UUPS proxy patterns for upgradeable contracts. If you're writing a custom implementation, you're also on the hook for implementing upgradability safely — a notoriously subtle problem. OpenZeppelin's proxies have been audited and used in production by major protocols.
The cases where you might legitimately deviate from OpenZeppelin are narrow: highly experimental token mechanics (like algorithmic rebasing), situations where gas optimization at the contract level is critical enough to justify custom implementation, or unusual governance structures that don't fit OpenZeppelin's access control models. For the vast majority of token deployments, OpenZeppelin is the right choice.
Deploying an OpenZeppelin ERC-20: Step by Step
Here's a complete walkthrough of deploying an OpenZeppelin ERC-20 token from scratch using Hardhat. This assumes you have Node.js installed and a MetaMask wallet with some Sepolia ETH for testing.
Step 1: Set up your project
mkdir my-token && cd my-token
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
npm install @openzeppelin/contracts
npx hardhat init
Choose "Create a JavaScript project" when prompted. This sets up the Hardhat project structure with a sample contract, test, and deployment script.
Step 2: Write your token contract
Create contracts/MyToken.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, ERC20Burnable, Ownable {
constructor(address initialOwner)
ERC20("My Token", "MTK")
Ownable(initialOwner)
{
// Mint 1,000,000 tokens to the deployer
_mint(initialOwner, 1_000_000 * 10 ** decimals());
}
// Only the owner can mint additional tokens
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
}
Step 3: Write a deployment script
Create scripts/deploy.js:
const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying with:", deployer.address);
const MyToken = await ethers.getContractFactory("MyToken");
const token = await MyToken.deploy(deployer.address);
await token.waitForDeployment();
console.log("MyToken deployed to:", await token.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Step 4: Configure Hardhat for Sepolia
Edit hardhat.config.js to add the Sepolia network and your private key (use an environment variable — never hardcode private keys):
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY],
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
};
Step 5: Deploy to Sepolia
npx hardhat run scripts/deploy.js --network sepolia
Copy the deployed contract address from the output. You'll need it for verification.
Step 6: Verify on Etherscan
npx hardhat verify --network sepolia DEPLOYED_CONTRACT_ADDRESS \
"YOUR_DEPLOYER_ADDRESS"
After verification, your contract's source code will be publicly visible on Etherscan, showing the world exactly what rules govern your token. This is an important trust signal for anyone evaluating your project. See the full guide on how to verify your contract on Etherscan for more detail on the verification process, including common errors and how to fix them.
If you'd rather skip the development environment setup entirely and deploy an OpenZeppelin-based token in minutes, you can create your own ERC-20 token using ERC Token Creator. The platform handles the contract generation, compilation, and deployment, and produces a contract that's identical in structure and security to what you'd write manually. You can still verify it on Etherscan afterward.
Frequently Asked Questions
Is it safe to use OpenZeppelin contracts in production?
Yes — OpenZeppelin is the industry standard precisely because it is safe for production use. The contracts have been audited multiple times by leading security firms, are used by the most security-conscious teams in the Ethereum ecosystem, and have protected billions of dollars in value without a critical vulnerability in the base contracts. That said, "safe base contracts" doesn't mean "your entire contract is safe." Any custom logic you add on top of OpenZeppelin should be thoroughly tested and, for any token with significant value, professionally audited.
Do I need to understand Solidity to create an ERC-20 token with OpenZeppelin?
Not if you use a no-code deployment platform like the ERC-20 token creator. The platform generates and deploys OpenZeppelin-based contracts based on your configuration. Understanding Solidity is valuable context — this guide gives you that context — but it's not a prerequisite for deployment. If you want to do custom modifications beyond what a no-code platform supports, you'll need Solidity skills or a developer to help.
What version of OpenZeppelin should I use?
Use the latest stable version (v5.x as of early 2025). OpenZeppelin v5 introduced significant improvements including better error handling with custom error types instead of revert strings (which saves gas), updated access control patterns, and Solidity 0.8.20 as the minimum required version. If you're starting a new project, there's no good reason to use v4 or earlier. Check the OpenZeppelin changelog before upgrading existing projects, as v5 made several breaking changes in the API.
What's the difference between ERC20Mintable and just using _mint directly?
Older versions of OpenZeppelin (v2 and v3) shipped preset contracts like ERC20Mintable. In current versions (v4+), OpenZeppelin removed these presets in favor of having you compose the behavior yourself by inheriting from the extension contracts and adding your own access-controlled mint function. This gives you more flexibility and makes the access control decision explicit. The _mint internal function is always available — you just decide who can call it by writing a public mint function with the appropriate modifier.
Can I create an ERC-20 token that charges a fee on every transfer?
Yes — you can override the _update internal function in OpenZeppelin to intercept every token movement and redirect a portion to a fee recipient address. This is called a "fee-on-transfer" token. However, be aware that this breaks compatibility with many DeFi protocols, which assume transfers move the exact amount specified. Uniswap, for example, handles fee-on-transfer tokens in its router, but many other protocols will behave incorrectly if the amount received doesn't match the amount sent. Think carefully about whether fee-on-transfer mechanics actually serve your tokenomics goals before implementing them.