Deep Dive

Gas Optimization Patterns for Base L2 Smart Contracts

Learn advanced techniques to minimize gas costs in your Base L2 smart contracts and save your users money.

Javery
··6 min read
Gas Optimization Patterns for Base L2 Smart Contracts

While Base L2 offers significantly lower gas costs than Ethereum mainnet, optimizing your smart contracts is still crucial for providing the best user experience. In this deep dive, we'll explore advanced gas optimization patterns.

Why Optimize on L2?

Even though Base L2 transactions cost a fraction of mainnet:

  • User experience matters - Every penny saved improves adoption
  • High-frequency operations - Costs add up for active dApps
  • Competitive advantage - Lower costs attract more users

Base L2 Gas Costs

On Base, you're typically paying 1-5% of what you'd pay on Ethereum mainnet. But with proper optimization, you can reduce that even further.

ℹ️

Pattern 1: Storage Optimization

Storage is expensive. Here's how to minimize it:

Use Smaller Data Types

// ❌ Wasteful - each variable uses a full slot (32 bytes)
contract Wasteful {
    uint256 a;
    uint256 b;
    uint256 c;
}
 
// ✅ Optimized - packed into a single slot
contract Optimized {
    uint128 a;
    uint64 b;
    uint64 c;
}

Pack Structs Carefully

// ❌ Not packed - uses 3 storage slots
struct User {
    uint256 id;        // slot 1
    address wallet;    // slot 2
    bool active;       // slot 3
}
 
// ✅ Packed - uses only 2 storage slots
struct User {
    uint256 id;        // slot 1
    address wallet;    // slot 2 (20 bytes)
    bool active;       // slot 2 (1 byte) - packed!
}

Packing Rule

Solidity packs variables into 32-byte slots from right to left. Order your struct fields from largest to smallest for optimal packing.

💡

Pattern 2: Calldata vs Memory

Understanding the difference can save significant gas:

// ❌ Expensive - copies to memory
function processArray(uint256[] memory data) external {
    for (uint256 i = 0; i < data.length; i++) {
        // Process data
    }
}
 
// ✅ Cheaper - reads directly from calldata
function processArray(uint256[] calldata data) external {
    for (uint256 i = 0; i < data.length; i++) {
        // Process data
    }
}

Gas saved: ~200 gas per array element

Pattern 3: Unchecked Arithmetic

In Solidity 0.8+, arithmetic operations check for overflow by default. When you're certain overflow won't occur, use unchecked:

// ❌ Default - includes overflow checks
function sum(uint256[] memory numbers) public pure returns (uint256) {
    uint256 total = 0;
    for (uint256 i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
    return total;
}
 
// ✅ Unchecked - skips overflow checks when safe
function sum(uint256[] memory numbers) public pure returns (uint256) {
    uint256 total = 0;
    for (uint256 i = 0; i < numbers.length; ) {
        total += numbers[i];
        unchecked { ++i; }
    }
    return total;
}

Use Carefully

Only use unchecked when you're absolutely certain overflow cannot occur. Bugs from unchecked arithmetic can be catastrophic.

⚠️

Pattern 4: Batch Operations

Combine multiple operations into single transactions:

contract TokenDistributor {
    // ❌ Multiple transactions required
    function distribute(address to, uint256 amount) external {
        token.transfer(to, amount);
    }
 
    // ✅ Single transaction for multiple recipients
    function batchDistribute(
        address[] calldata recipients,
        uint256[] calldata amounts
    ) external {
        require(recipients.length == amounts.length, "Length mismatch");
 
        for (uint256 i = 0; i < recipients.length; ) {
            token.transfer(recipients[i], amounts[i]);
            unchecked { ++i; }
        }
    }
}

Gas saved: 21,000 gas per transaction (base transaction cost)

Pattern 5: Events vs Storage

For data that doesn't need to be read on-chain, use events instead of storage:

// ❌ Expensive storage
mapping(uint256 => string) public messages;
 
function sendMessage(uint256 id, string memory message) external {
    messages[id] = message; // ~20,000 gas
}
 
// ✅ Cheaper event emission
event MessageSent(uint256 indexed id, string message);
 
function sendMessage(uint256 id, string memory message) external {
    emit MessageSent(id, message); // ~2,000 gas
}

Gas saved: ~18,000 gas per message

Event Best Practice

Use events for historical data that only needs to be accessed off-chain. Use storage for data that contracts need to read.

Pattern 6: Short-Circuit Evaluation

Order conditions strategically:

// ❌ Always checks expensive condition
function validate(address user) external view returns (bool) {
    return expensiveCheck(user) && cheapCheck(user);
}
 
// ✅ Checks cheap condition first
function validate(address user) external view returns (bool) {
    return cheapCheck(user) && expensiveCheck(user);
}

If cheapCheck() fails, expensiveCheck() never runs!

Pattern 7: Minimize SLOAD Operations

Each storage read (SLOAD) costs gas. Cache storage variables:

contract Counter {
    uint256 public count;
 
    // ❌ Multiple SLOADs
    function increment() external {
        count = count + 1;  // SLOAD
        require(count < 100, "Max reached");  // SLOAD
        emit Incremented(count);  // SLOAD
    }
 
    // ✅ Single SLOAD
    function increment() external {
        uint256 newCount = count + 1;  // SLOAD once
        require(newCount < 100, "Max reached");
        count = newCount;  // SSTORE
        emit Incremented(newCount);
    }
}

Real-World Example: Optimized NFT Minting

Let's apply these patterns to an NFT contract:

contract OptimizedNFT {
    // ✅ Packed storage
    struct TokenData {
        address owner;     // 20 bytes
        uint64 mintTime;   // 8 bytes
        uint32 tokenId;    // 4 bytes
    } // Total: 32 bytes (1 slot!)
 
    mapping(uint256 => TokenData) private _tokens;
    uint32 private _totalSupply;
 
    event Minted(address indexed to, uint32 tokenId);
 
    // ✅ Optimized minting
    function mint(address to) external returns (uint32) {
        uint32 tokenId;
        unchecked {
            tokenId = ++_totalSupply;
        }
 
        _tokens[tokenId] = TokenData({
            owner: to,
            mintTime: uint64(block.timestamp),
            tokenId: tokenId
        });
 
        emit Minted(to, tokenId);
        return tokenId;
    }
 
    // ✅ Batch minting
    function batchMint(
        address[] calldata recipients
    ) external returns (uint32[] memory) {
        uint32 startId = _totalSupply;
        uint32[] memory tokenIds = new uint32[](recipients.length);
 
        for (uint256 i = 0; i < recipients.length; ) {
            uint32 tokenId;
            unchecked {
                tokenId = ++_totalSupply;
                tokenIds[i] = tokenId;
            }
 
            _tokens[tokenId] = TokenData({
                owner: recipients[i],
                mintTime: uint64(block.timestamp),
                tokenId: tokenId
            });
 
            emit Minted(recipients[i], tokenId);
 
            unchecked { ++i; }
        }
 
        return tokenIds;
    }
}

Measuring Gas Savings

Always measure your optimizations! Use Hardhat's gas reporter:

import "hardhat-gas-reporter";
 
export default {
  gasReporter: {
    enabled: true,
    currency: 'USD',
    coinmarketcap: process.env.CMC_API_KEY,
  },
};

Run your tests:

npx hardhat test

OnBase Dashboard

Monitor and manage all your Base L2 applications

Monitor Gas Usage
📊

Tools and Resources

  • Hardhat Gas Reporter - Track gas usage in tests
  • Tenderly - Debug and optimize transactions
  • Etherscan Gas Tracker - Monitor network gas prices
  • OpenZeppelin Contracts - Study optimized implementations

Key Takeaways

  1. Storage is expensive - Pack variables, use events when possible
  2. Batch operations - Combine multiple actions into one transaction
  3. Use calldata - For read-only external function parameters
  4. Cache storage reads - Minimize SLOAD operations
  5. Unchecked arithmetic - When overflow is impossible
  6. Measure everything - Use gas reporters to track improvements

Keep Learning

Gas optimization is an ongoing process. Stay updated with the latest Solidity features and patterns.

ℹ️

Next Steps

Happy optimizing! ⚡

Share:

Related Posts