Gas optimization saved us $12K per month on one contract. Before: 180K gas per transaction. After: 65K gas. Here's exactly what we changed and how you can apply the same techniques.
Why Gas Optimization Matters
At scale, gas adds up fast.
Real example from our NFT project:
| Metric | Before | After |
|---|---|---|
| Gas per mint | 180,000 | 65,000 |
| Daily mints | 500 | 500 |
| Gas price (avg) | 30 gwei | 30 gwei |
| Daily cost | $810 | $292 |
| Monthly cost | $24,300 | $8,760 |
| Monthly savings | $15,540 |
That's $186K per year saved. Worth the optimization effort.
Storage is Expensive
The biggest gas sink is storage. Every SSTORE (storage write) costs 20,000 gas for new slots, 5,000 for updates.
Expensive pattern:
// BAD: Multiple storage writes
function mint(address to) public {
totalSupply++; // SSTORE: 5,000 gas
balanceOf[to]++; // SSTORE: 5,000+ gas
ownerOf[tokenId] = to; // SSTORE: 20,000 gas
tokenId++; // SSTORE: 5,000 gas
emit Transfer(...);
}
// Total: ~35,000+ gas just for storage
Optimized:
// BETTER: Cache in memory, single storage write
function mint(address to) public {
uint256 _tokenId = tokenId; // SLOAD once
// Batch update in single slot if possible
unchecked {
_mint(to, _tokenId);
tokenId = _tokenId + 1; // Single SSTORE
}
}
Pack Your Structs
Solidity uses 32-byte storage slots. Pack variables to fit in fewer slots.
Wasteful:
// BAD: Each variable uses its own slot
struct NFTData {
uint256 tokenId; // Slot 0 (32 bytes)
address owner; // Slot 1 (20 bytes, wastes 12)
uint256 price; // Slot 2 (32 bytes)
bool forSale; // Slot 3 (1 byte, wastes 31)
uint8 rarity; // Slot 4 (1 byte, wastes 31)
}
// Total: 5 slots = 5 * 20,000 = 100,000 gas to initialize
Packed:
// GOOD: Variables packed into fewer slots
struct NFTData {
uint256 tokenId; // Slot 0 (32 bytes)
uint256 price; // Slot 1 (32 bytes)
address owner; // Slot 2 starts (20 bytes)
bool forSale; // Slot 2 continues (1 byte)
uint8 rarity; // Slot 2 continues (1 byte)
// 10 bytes left in slot 2
}
// Total: 3 slots = 3 * 20,000 = 60,000 gas
// Savings: 40,000 gas per struct
Use calldata Instead of memory
For read-only function parameters, calldata is cheaper.
Expensive:
// BAD: Copies array to memory
function batchTransfer(
address[] memory recipients, // Copies to memory
uint256[] memory amounts
) public {
for (uint i = 0; i < recipients.length; i++) {
transfer(recipients[i], amounts[i]);
}
}
Cheap:
// GOOD: Reads directly from calldata
function batchTransfer(
address[] calldata recipients, // No copy
uint256[] calldata amounts
) external {
for (uint i = 0; i < recipients.length; i++) {
transfer(recipients[i], amounts[i]);
}
}
Gas savings:
| Array size | memory | calldata | Savings |
|---|---|---|---|
| 10 items | 5,000 | 500 | 4,500 |
| 100 items | 50,000 | 5,000 | 45,000 |
| 1000 items | 500,000 | 50,000 | 450,000 |
Unchecked Arithmetic
Solidity 0.8+ has built-in overflow checks. When you know overflow is impossible, skip the checks.
With checks (default):
function increment() public {
counter++; // Includes overflow check
// ~200 gas for the check
}
Without checks:
function increment() public {
unchecked {
counter++; // No overflow check
}
// Saves ~200 gas
}
When it's safe:
// SAFE: i can't overflow before running out of gas
for (uint256 i = 0; i < array.length;) {
// process array[i]
unchecked { ++i; }
}
// SAFE: tokenId won't reach 2^256 in practice
unchecked {
tokenId++;
}
// DANGEROUS: User input could overflow
unchecked {
userBalance += untrustedInput; // DON'T DO THIS
}
ERC721A for NFT Collections
Standard ERC721 mints are expensive. ERC721A batches efficiently.
Standard ERC721 batch mint:
// OpenZeppelin ERC721
function mintBatch(address to, uint256 quantity) public {
for (uint i = 0; i < quantity; i++) {
_safeMint(to, tokenId++);
// Each iteration: ~90,000 gas
}
}
// 10 NFTs = 900,000 gas
ERC721A batch mint:
// ERC721A
function mintBatch(address to, uint256 quantity) public {
_mint(to, quantity);
// Single operation for any quantity
}
// 10 NFTs = 150,000 gas (6x cheaper)
Real comparison:
| Quantity | ERC721 | ERC721A | Savings |
|---|---|---|---|
| 1 | 90,000 | 60,000 | 33% |
| 5 | 450,000 | 75,000 | 83% |
| 10 | 900,000 | 150,000 | 83% |
| 20 | 1,800,000 | 200,000 | 89% |
Loop Optimizations
Loops are gas hot spots. Small improvements multiply.
Before optimization:
function processArray(uint256[] memory data) public {
for (uint256 i = 0; i < data.length; i++) {
// data.length read from memory each iteration
values[i] = data[i] * multiplier;
// multiplier read from storage each iteration
}
}
After optimization:
function processArray(uint256[] calldata data) external {
uint256 len = data.length; // Cache length
uint256 mult = multiplier; // Cache storage variable
for (uint256 i; i < len;) { // Start at 0 implicitly
values[i] = data[i] * mult;
unchecked { ++i; } // Unchecked increment
}
}
Gas savings per iteration:
| Optimization | Savings |
|---|---|
| Cache length | ~100 gas |
| Cache storage | ~2,100 gas |
| Implicit zero | ~3 gas |
| ++i vs i++ | ~5 gas |
| unchecked | ~200 gas |
| Total per iteration | ~2,400 gas |
For 100 iterations: 240,000 gas saved.
Short-Circuit Conditions
Order conditions by likelihood and cost.
Expensive first (bad):
// BAD: Expensive check first
require(
verifyMerkleProof(proof, leaf) && // Expensive
msg.value >= price, // Cheap
"Invalid"
);
Cheap first (good):
// GOOD: Cheap check first, fails fast
require(msg.value >= price, "Insufficient payment");
require(verifyMerkleProof(proof, leaf), "Invalid proof");
If the cheap check fails, you save the gas of the expensive check.
Events Are Cheaper Than Storage
Sometimes you don't need on-chain data. Emit events instead.
Expensive (storage):
mapping(uint256 => uint256) public mintTimestamps;
function mint() public {
mintTimestamps[tokenId] = block.timestamp; // 20,000 gas
_mint(msg.sender, tokenId++);
}
Cheap (events):
event Minted(uint256 indexed tokenId, uint256 timestamp);
function mint() public {
emit Minted(tokenId, block.timestamp); // ~1,500 gas
_mint(msg.sender, tokenId++);
}
When to use events:
- Historical data that's read off-chain
- Analytics and tracking
- Data that doesn't need on-chain verification
Testing Gas Costs
Measure before and after every optimization.
Hardhat gas reporter:
// hardhat.config.js
module.exports = {
gasReporter: {
enabled: true,
currency: 'USD',
gasPrice: 30,
coinmarketcap: process.env.CMC_API_KEY
}
};
Foundry gas snapshots:
forge snapshot
# Creates .gas-snapshot file
forge snapshot --diff
# Shows gas changes from last snapshot
Inline gas measurement:
function testGas() public {
uint256 gasBefore = gasleft();
// Code to measure
contract.mint(address(this));
uint256 gasUsed = gasBefore - gasleft();
console.log("Gas used:", gasUsed);
}
Complete Optimized Contract Example
Before and after for an NFT mint function.
Before (180K gas):
function mint(address to, uint256 quantity) public payable {
require(quantity > 0, "Zero quantity");
require(quantity <= maxPerTx, "Exceeds max");
require(totalSupply + quantity <= maxSupply, "Sold out");
require(msg.value >= price * quantity, "Insufficient payment");
for (uint256 i = 0; i < quantity; i++) {
_safeMint(to, totalSupply);
totalSupply++;
}
}
After (65K gas):
function mint(address to, uint256 quantity) external payable {
// Cache storage reads
uint256 _totalSupply = totalSupply;
uint256 _price = price;
uint256 _maxSupply = maxSupply;
// Checks
require(quantity != 0, "Zero quantity");
require(quantity <= maxPerTx, "Exceeds max");
require(_totalSupply + quantity <= _maxSupply, "Sold out");
require(msg.value >= _price * quantity, "Insufficient payment");
// Use ERC721A-style batch mint
_mint(to, quantity);
// Single storage write
unchecked {
totalSupply = _totalSupply + quantity;
}
}
Savings breakdown:
| Optimization | Gas Saved |
|---|---|
| external vs public | 200 |
| Cache storage (4 vars) | 8,000 |
| ERC721A batch | 80,000 |
| unchecked increment | 400 |
| Loop removal | 25,000 |
| Total | ~115,000 |
Resources
Tools:
- Hardhat Gas Reporter - Gas reporting
- Foundry - Gas snapshots
- Tenderly - Transaction analysis
- Token Cost Calculator - Cost estimation
Libraries:
Learning:
- Solidity Gas Optimizations - Opcode costs
- OpenZeppelin - Best practices
Development:
- AllThingsWeb3 - Smart contract development
- Hardhat - Development environment