# OpenZeppelin vs Solady — Which Library for Gas Optimization?
OpenZeppelin is the industry standard for Solidity contracts — audited, battle-tested, and trusted. But Solady, a newer library by Vectorized, optimizes aggressively for gas savings. Let's compare.
The Libraries
OpenZeppelin
- Focus: Security, readability, standards compliance
- Audited: Multiple audits, 8+ years in production
- Gas: Optimized, but prioritizes safety over gas
- Use case: Most projects, especially when audited code is critical
Solady
- Focus: Extreme gas optimization
- Audited: Newer, growing audit coverage
- Gas: Heavily optimized, uses assembly
- Use case: High-volume contracts, L2s, gas-critical apps
Gas Comparison: ERC20
OpenZeppelin ERC20
// @openzeppelin/contracts/token/ERC20/ERC20.sol
function transfer(address to, uint256 amount) public virtual returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
Gas cost: ~51,000 gas for first transfer, ~34,000 for subsequent
Solady ERC20
// solady/src/tokens/ERC20.sol
function transfer(address to, uint256 amount) public virtual returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
function _transfer(address from, address to, uint256 amount) internal virtual {
assembly {
let fromBalance := sload(add(_balances.slot, from))
if gt(amount, fromBalance) {
mstore(0x00, 0xf4d678b8) // InsufficientBalance()
revert(0x1c, 0x04)
}
sstore(add(_balances.slot, from), sub(fromBalance, amount))
sstore(add(_balances.slot, to), add(sload(add(_balances.slot, to)), amount))
// Emit Transfer event
mstore(0x00, amount)
log3(0x00, 0x20, _TRANSFER_EVENT_SIGNATURE, from, to)
}
}
Gas cost: ~48,000 gas for first transfer, ~31,000 for subsequent
Savings: ~3,000 gas per transfer (9% cheaper)
Gas Comparison: ERC721
OpenZeppelin ERC721
function transferFrom(address from, address to, uint256 tokenId) public virtual {
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
_transfer(from, to, tokenId);
}
function _transfer(address from, address to, uint256 tokenId) internal virtual {
require(ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
require(to != address(0), "ERC721: transfer to the zero address");
_beforeTokenTransfer(from, to, tokenId, 1);
delete _tokenApprovals[tokenId];
unchecked {
_balances[from] -= 1;
_balances[to] += 1;
}
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
_afterTokenTransfer(from, to, tokenId, 1);
}
Gas cost: ~76,000 gas
Solady ERC721
function transferFrom(address from, address to, uint256 id) public virtual {
assembly {
let ownerAndApproval := sload(add(_ownersAndApprovals.slot, id))
let owner := and(ownerAndApproval, _BITMASK_ADDRESS)
if iszero(eq(owner, from)) {
mstore(0x00, 0xa1148100) // TransferFromIncorrectOwner()
revert(0x1c, 0x04)
}
// Check approval
let approved := shr(160, ownerAndApproval)
if iszero(or(eq(caller(), owner), eq(caller(), approved))) {
if iszero(sload(add(add(_operatorApprovals.slot, owner), caller()))) {
mstore(0x00, 0x4b6e7f18) // NotOwnerNorApproved()
revert(0x1c, 0x04)
}
}
// Transfer
sstore(add(_balances.slot, from), sub(sload(add(_balances.slot, from)), 1))
sstore(add(_balances.slot, to), add(sload(add(_balances.slot, to)), 1))
sstore(add(_ownersAndApprovals.slot, id), to)
log4(0x00, 0x00, _TRANSFER_EVENT_SIGNATURE, from, to, id)
}
}
Gas cost: ~68,000 gas
Savings: ~8,000 gas per transfer (11% cheaper)
Feature Comparison
| Feature | OpenZeppelin | Solady |
|---------|-------------|--------|
| ERC20 | Full featured | Full featured |
| ERC721 | Full featured | Full featured |
| ERC1155 | ✅ | ✅ |
| Access Control | Ownable, AccessControl | Ownable, auth patterns |
| Upgradeability | UUPS, Transparent, Beacon | UUPS variants |
| Utils | SafeMath, Address, Strings | LibBit, LibMap, LibClone |
| Assembly usage | Minimal | Heavy |
| Readability | High | Medium (due to assembly) |
| Audit coverage | Extensive | Growing |
When to Use OpenZeppelin
1. Audit Requirements
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
constructor() ERC20("MyToken", "MTK") {}
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
}
Why: Auditors are familiar with OZ, security track record is proven.
2. Upgradeable Contracts
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract MyTokenUpgradeable is Initializable, ERC20Upgradeable {
function initialize() public initializer {
__ERC20_init("MyToken", "MTK");
}
}
Why: OZ has comprehensive upgradeable variants.
3. Extensions and Hooks
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
contract MyToken is ERC20, ERC20Burnable, ERC20Pausable {
// Hooks are easy to override
function _beforeTokenTransfer(
address from,
address to,
uint256 amount
) internal override(ERC20, ERC20Pausable) {
super._beforeTokenTransfer(from, to, amount);
// Custom logic
}
}
Why: OZ's hook system is mature and composable.
When to Use Solady
1. High Transaction Volume
import "solady/src/tokens/ERC20.sol";
contract HighVolumeToken is ERC20 {
function name() public pure override returns (string memory) {
return "HighVolume";
}
function symbol() public pure override returns (string memory) {
return "HVT";
}
// Saves 3k gas per transfer
// 1M transfers = 3B gas saved = ~$500+ at 20 gwei
}
Why: Gas savings compound at scale.
2. L2 Deployments
import "solady/src/tokens/ERC721.sol";
contract L2NFT is ERC721 {
// L2s have lower gas costs but higher throughput
// Solady's optimizations still matter for tx/sec
}
Why: Even on cheap L2s, optimization improves throughput.
3. Gas-Critical Apps
import "solady/src/utils/LibClone.sol";
contract Factory {
address immutable implementation;
function createClone() external returns (address) {
// Solady's LibClone is 10x cheaper than OZ's Clones
return LibClone.clone(implementation);
}
}
Why: Solady's utilities are hyper-optimized.
Hybrid Approach
You can mix libraries:
import "@openzeppelin/contracts/access/Ownable.sol"; // Familiar, audited
import "solady/src/tokens/ERC20.sol"; // Gas-optimized core
contract HybridToken is ERC20, Ownable {
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
}
Real-World Examples
OpenZeppelin Users
- USDC: Audited, industry standard
- Uniswap V2/V3: Trusted, proven
- Compound: Security-first
Solady Users
- Blur: NFT marketplace (high volume)
- Zora: L2 NFTs
- Farcaster: Social protocol (L2)
Benchmarks
| Operation | OpenZeppelin | Solady | Savings |
|-----------|-------------|--------|---------|
| ERC20 transfer | 34,000 | 31,000 | 9% |
| ERC721 transfer | 76,000 | 68,000 | 11% |
| ERC721 mint | 92,000 | 84,000 | 9% |
| Clone creation | 45,000 | 4,500 | 90% |
| Ownable check | 2,400 | 2,100 | 12% |
Key Takeaways
- Proven security track record
- Extensive audit coverage
- Rich ecosystem of extensions
- 9-11% cheaper on standard ops
- 90% cheaper on specialized utils
- Growing audit coverage
- Use OZ for access control, upgradeability
- Use Solady for tokens, utilities
- Transaction volume (high → Solady)
- Audit requirements (strict → OZ)
- L2 deployment (Solady edge)
- Team familiarity (OZ default)
forge snapshot --diff
Both libraries are excellent. OpenZeppelin is the safe default; Solady is the gas-conscious alternative. Choose based on your priorities: security-first or gas-first.