$cat/blog/gas-optimization.md1422 words | 8 min
//

Gas Optimization for Smart Contracts: Save Thousands Per Month

#gas optimization smart contracts#Solidity gas optimization#reduce gas costs
article.md

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:

MetricBeforeAfter
Gas per mint180,00065,000
Daily mints500500
Gas price (avg)30 gwei30 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 sizememorycalldataSavings
10 items5,0005004,500
100 items50,0005,00045,000
1000 items500,00050,000450,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:

QuantityERC721ERC721ASavings
190,00060,00033%
5450,00075,00083%
10900,000150,00083%
201,800,000200,00089%

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:

OptimizationSavings
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:

OptimizationGas Saved
external vs public200
Cache storage (4 vars)8,000
ERC721A batch80,000
unchecked increment400
Loop removal25,000
Total~115,000

Resources

Tools:

Libraries:

Learning:

Development:

continue_learning.sh

# Keep building your smart contract knowledge

$ More guides from 100+ contract deployments generating $250M+ volume