Claude Code Plugins

Community-maintained marketplace

Feedback

Gas optimization techniques and patterns for efficient Solidity smart contracts. Use when optimizing contract gas costs or reviewing code for efficiency improvements.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name gas-optimization
description Gas optimization techniques and patterns for efficient Solidity smart contracts. Use when optimizing contract gas costs or reviewing code for efficiency improvements.

Gas Optimization Skill

This skill provides techniques, patterns, and best practices for optimizing gas costs in Solidity smart contracts.

When to Use

Use this skill when:

  • Optimizing contract gas costs
  • Reviewing code for efficiency improvements
  • Preparing contracts for production deployment
  • Reducing transaction costs for users
  • Optimizing storage usage
  • Improving loop efficiency

Important: Always prioritize security over gas optimization. Only optimize after ensuring correctness and security.

Gas Cost Basics

Operation Costs (Approximate)

Operation Gas Cost
Addition/Subtraction 3
Multiplication/Division 5
SLOAD (storage read) 2,100
SSTORE (storage write, zero to non-zero) 20,000
SSTORE (storage write, non-zero to non-zero) 5,000
SSTORE (storage write, non-zero to zero) 15,000 refund
Memory expansion Quadratic
LOG0-LOG4 375 + 375 * topics
CALL 100-9,000
CREATE 32,000

Key Principles

  1. Storage is expensive - Minimize SLOAD/SSTORE
  2. Memory is cheap - Use for temporary data
  3. Calldata is cheapest - Use for read-only function parameters
  4. Smaller is better - Pack variables, use smaller types
  5. Batch operations - Combine multiple operations
  6. Avoid redundancy - Cache values, eliminate duplicate code

Storage Optimization

1. Variable Packing

Pack variables into 32-byte slots:

// ❌ Bad: Uses 3 storage slots (96 bytes)
contract Inefficient {
    uint8 a;      // slot 0 (wastes 31 bytes)
    uint256 b;    // slot 1
    uint8 c;      // slot 2 (wastes 31 bytes)
}

// ✅ Good: Uses 2 storage slots (64 bytes)
contract Efficient {
    uint8 a;      // slot 0
    uint8 c;      // slot 0 (packed)
    uint256 b;    // slot 1
}

// ✅ Better: Pack related variables
contract BetterPacking {
    address owner;    // 20 bytes - slot 0
    uint96 balance;   // 12 bytes - slot 0 (packed, total 32 bytes)

    uint128 value1;   // 16 bytes - slot 1
    uint128 value2;   // 16 bytes - slot 1 (packed, total 32 bytes)
}

Savings: ~15,000 gas per avoided storage slot

2. Use Smaller Types When Possible

// ❌ Unnecessary uint256 for small values
uint256 public percentage;  // 0-100, wastes space

// ✅ Use appropriate size
uint8 public percentage;    // 0-255, sufficient for percentages

// Timestamps
uint32 public timestamp;    // Valid until year 2106
uint40 public timestamp;    // Valid until year 36812

// Counters
uint32 public counter;      // Supports 4.29 billion items

Warning: Smaller types don't save gas in function parameters or local variables, only in storage!

3. Caching Storage Variables

// ❌ Bad: Multiple storage reads (6,300 gas)
function bad() public view returns (uint256) {
    return storageValue + storageValue + storageValue;
}

// ✅ Good: Cache in memory (2,103 gas)
function good() public view returns (uint256) {
    uint256 cached = storageValue;  // 1 SLOAD
    return cached + cached + cached;
}

Pattern: Cache before loops:

// ❌ Bad: SLOAD in every iteration
function sumBad() public view returns (uint256) {
    uint256 sum = 0;
    for (uint256 i = 0; i < items.length; i++) {
        sum += multiplier * items[i];  // SLOAD multiplier each time
    }
    return sum;
}

// ✅ Good: Cache outside loop
function sumGood() public view returns (uint256) {
    uint256 sum = 0;
    uint256 _multiplier = multiplier;  // Cache once
    for (uint256 i = 0; i < items.length; i++) {
        sum += _multiplier * items[i];
    }
    return sum;
}

4. Use Constants and Immutables

// ❌ Bad: Storage variable (2,100 gas per read)
uint256 public maxSupply = 1000000;

// ✅ Good: Constant (no storage, embedded in bytecode)
uint256 public constant MAX_SUPPLY = 1000000;

// ✅ Good: Immutable (set once in constructor, cheap to read)
address public immutable owner;

constructor() {
    owner = msg.sender;
}

Savings:

  • constant: ~2,100 gas per read vs storage
  • immutable: ~2,000 gas per read vs storage

5. Short-Circuit Storage Updates

// ❌ Always writes (5,000-20,000 gas)
function setBad(uint256 newValue) public {
    value = newValue;
}

// ✅ Only write if changed (saves gas when unchanged)
function setGood(uint256 newValue) public {
    if (value != newValue) {
        value = newValue;
    }
}

6. Delete to Get Refunds

// Clear storage to get refund (15,000 gas)
delete balances[user];  // Sets to 0

// Or explicitly
balances[user] = 0;

Note: Refund is capped at 50% of gas used in transaction

Function Optimization

1. Function Visibility

// ❌ public functions cost more
function getData() public view returns (uint256) {
    return data;
}

// ✅ external is cheaper (saves ~200 gas per call)
function getData() external view returns (uint256) {
    return data;
}

Rule: Use external if function is only called externally

2. Payable Functions

// ❌ Non-payable has additional checks
function transfer(address to, uint256 amount) public {
    // Costs extra to check msg.value == 0
}

// ✅ Payable skips check (saves ~24 gas)
function transfer(address to, uint256 amount) public payable {
    // Only use if function legitimately accepts ETH
}

Warning: Only use payable if function should accept ETH!

3. Short-Circuit Conditions

// ✅ Put cheap checks first
require(msg.sender == owner && expensiveCheck(), "Failed");

// Expensive check only runs if sender is owner

4. Custom Errors

// ❌ String errors are expensive
require(balance >= amount, "Insufficient balance");

// ✅ Custom errors save gas (~50 gas)
error InsufficientBalance(uint256 balance, uint256 required);

if (balance < amount) {
    revert InsufficientBalance(balance, amount);
}

Savings: ~50 gas per revert + deployment cost savings

Loop Optimization

1. Cache Array Length

// ❌ Bad: Reads length every iteration
for (uint256 i = 0; i < array.length; i++) {
    // Process
}

// ✅ Good: Cache length
uint256 length = array.length;
for (uint256 i = 0; i < length; i++) {
    // Process
}

Savings: ~100 gas per iteration for storage arrays

2. Unchecked Increments

// ❌ Checked increment
for (uint256 i = 0; i < length; i++) {
    // Overflow check costs ~30 gas
}

// ✅ Unchecked increment (safe if bounds known)
for (uint256 i = 0; i < length;) {
    // Process

    unchecked {
        ++i;  // Saves ~30 gas per iteration
    }
}

3. ++i vs i++

// ✅ Prefix increment cheaper
for (uint256 i = 0; i < length; ++i) {
    // Saves ~5 gas per iteration
}

// ❌ Postfix increment more expensive
for (uint256 i = 0; i < length; i++) {
    // Costs extra gas
}

4. Avoid Storage Array Iteration

// ❌ Very expensive: Storage array iteration
uint256[] public items;

function sumBad() public view returns (uint256) {
    uint256 sum = 0;
    for (uint256 i = 0; i < items.length; i++) {
        sum += items[i];  // SLOAD every iteration
    }
    return sum;
}

// ✅ Better: Use mapping with counter
mapping(uint256 => uint256) public items;
uint256 public itemCount;

// ✅ Or load to memory first
function sumGood() public view returns (uint256) {
    uint256[] memory _items = items;  // Load once
    uint256 sum = 0;
    for (uint256 i = 0; i < _items.length; i++) {
        sum += _items[i];  // Memory access
    }
    return sum;
}

Memory vs Calldata

1. Use Calldata for Read-Only Parameters

// ❌ memory copies data (expensive)
function processBad(uint256[] memory data) external {
    uint256 sum = 0;
    for (uint256 i = 0; i < data.length; i++) {
        sum += data[i];
    }
}

// ✅ calldata reads directly (cheap)
function processGood(uint256[] calldata data) external {
    uint256 sum = 0;
    for (uint256 i = 0; i < data.length; i++) {
        sum += data[i];
    }
}

Savings: Significant for large arrays (~50-100 gas per element)

2. Return Memory Efficiently

// ❌ Creates unnecessary array
function getBad() public view returns (uint256[] memory) {
    uint256[] memory result = new uint256[](100);
    // Fill array
    return result;
}

// ✅ Return only needed data
function getGood(uint256 start, uint256 count) public view returns (uint256[] memory) {
    uint256[] memory result = new uint256[](count);
    // Fill with requested range
    return result;
}

Data Structure Optimization

1. Mappings vs Arrays

// ✅ Mappings: O(1) access, good for sparse data
mapping(uint256 => User) public users;

// ✅ Arrays: Good for iteration, dense data
User[] public userArray;

Trade-offs:

  • Mappings: Fast access, can't iterate, can't get length
  • Arrays: Iterable, expensive to grow, costly iteration

2. Nested Mappings vs Structs

// ❌ Multiple mappings
mapping(address => uint256) public balances;
mapping(address => uint256) public rewards;
mapping(address => uint256) public stakes;

// ✅ Single mapping with struct
struct UserData {
    uint256 balance;
    uint256 reward;
    uint256 stake;
}
mapping(address => UserData) public userData;

// Access: userData[user].balance (1 SLOAD for struct pointer)

3. Bit Packing for Booleans

// ❌ 8 booleans = 8 storage slots
bool public flag1;
bool public flag2;
// ... 8 total

// ✅ Use single uint256 with bit operations
uint256 public flags;  // Can store 256 booleans

function setFlag(uint256 index, bool value) public {
    if (value) {
        flags |= (1 << index);   // Set bit
    } else {
        flags &= ~(1 << index);  // Clear bit
    }
}

function getFlag(uint256 index) public view returns (bool) {
    return (flags & (1 << index)) != 0;
}

Advanced Techniques

1. Assembly for Storage Access

// ✅ Assembly can be more efficient for storage
function getValueAsm(uint256 slot) public view returns (uint256 value) {
    assembly {
        value := sload(slot)
    }
}

function setValueAsm(uint256 slot, uint256 value) public {
    assembly {
        sstore(slot, value)
    }
}

Warning: Use assembly cautiously - easy to make mistakes!

2. Batch Operations

// ❌ Multiple transactions
function transferMultipleTx(address[] calldata recipients, uint256[] calldata amounts) external {
    for (uint256 i = 0; i < recipients.length; i++) {
        transfer(recipients[i], amounts[i]);
    }
}

// ✅ Single transaction with batch
function batchTransfer(address[] calldata recipients, uint256[] calldata amounts) external {
    require(recipients.length == amounts.length, "Length mismatch");

    for (uint256 i = 0; i < recipients.length;) {
        _transfer(msg.sender, recipients[i], amounts[i]);

        unchecked { ++i; }
    }
}

3. Events vs Storage

// ❌ Store historical data on-chain
uint256[] public historicalPrices;

// ✅ Emit events instead (much cheaper)
event PriceUpdated(uint256 price, uint256 timestamp);

function updatePrice(uint256 newPrice) external {
    currentPrice = newPrice;
    emit PriceUpdated(newPrice, block.timestamp);
}

Savings: Events are ~1/10 the cost of storage

4. Lazy Initialization

// ❌ Initialize all at once
constructor() {
    for (uint256 i = 0; i < 100; i++) {
        data[i] = initialValue;
    }
}

// ✅ Initialize on demand
mapping(uint256 => uint256) private data;
bool private initialized;

function getData(uint256 index) public returns (uint256) {
    if (!data[index]) {
        data[index] = initialValue;
    }
    return data[index];
}

Compiler Optimization

1. Optimizer Settings

foundry.toml:

[profile.default]
optimizer = true
optimizer_runs = 200  # Balance deployment vs runtime cost

[profile.production]
optimizer_runs = 10000  # Optimize for runtime (contracts called often)

[profile.deployment]
optimizer_runs = 1  # Optimize for deployment (one-time contracts)

hardhat.config.js:

module.exports = {
  solidity: {
    version: "0.8.30",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      },
      viaIR: true  // Use IR-based compiler (can reduce gas further)
    }
  }
};

2. Solidity Version

// ✅ Use latest stable version for best optimizations
pragma solidity ^0.8.30;

Newer versions have better:

  • Optimizer improvements
  • Gas optimizations
  • Built-in overflow checks

Gas Reporting

Foundry

# Gas report in tests
forge test --gas-report

# Gas snapshot
forge snapshot

# Compare snapshots
forge snapshot --diff

Hardhat

// hardhat.config.js
require("hardhat-gas-reporter");

module.exports = {
  gasReporter: {
    enabled: true,
    currency: "USD",
    coinmarketcap: process.env.CMC_API_KEY
  }
};

Common Anti-Patterns

❌ 1. Redundant Checks

// ❌ Redundant: SafeMath when using Solidity 0.8+
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
using SafeMath for uint256;

// ✅ Built-in overflow protection
uint256 result = a + b;  // Reverts on overflow in 0.8+

❌ 2. String Concatenation

// ❌ Very expensive
string memory result = string(abi.encodePacked(str1, str2, str3));

// ✅ Avoid strings in contracts when possible
// Use events or off-chain concatenation

❌ 3. Storing Large Data

// ❌ Don't store large data on-chain
string[] public descriptions;  // Each string costs thousands of gas

// ✅ Store hash and keep data off-chain
bytes32[] public descriptionHashes;

❌ 4. Unbounded Loops

// ❌ Can hit gas limit
function processAll() public {
    for (uint256 i = 0; i < users.length; i++) {
        // Process
    }
}

// ✅ Add pagination
function processBatch(uint256 start, uint256 count) public {
    uint256 end = start + count;
    if (end > users.length) end = users.length;

    for (uint256 i = start; i < end; i++) {
        // Process
    }
}

Optimization Checklist

Storage

  • Variables packed into 32-byte slots
  • Used smallest appropriate type for storage variables
  • Constants used instead of storage where possible
  • Immutables used for constructor-set values
  • Cached storage variables accessed multiple times
  • Unnecessary storage writes removed
  • Deletions used to get gas refunds

Functions

  • External used instead of public where possible
  • Custom errors instead of string errors
  • Short-circuit boolean conditions
  • Payable used where appropriate (only if accepts ETH)
  • Function visibility optimized

Loops

  • Array length cached
  • Unchecked increment used (where safe)
  • ++i used instead of i++
  • Storage access minimized in loops
  • Unbounded loops avoided

Memory/Calldata

  • Calldata used for read-only array parameters
  • Memory vs storage trade-offs considered
  • Unnecessary memory allocations removed

General

  • Batch operations used where possible
  • Events used instead of storage for historical data
  • Compiler optimizer enabled
  • Latest stable Solidity version used
  • Gas report reviewed

Measurement and Benchmarking

Before Optimization

# Baseline gas report
forge test --gas-report > gas-before.txt
forge snapshot --snap baseline.snap

After Optimization

# Compare gas usage
forge test --gas-report > gas-after.txt
forge snapshot --diff baseline.snap

Gas Profiling

# Detailed gas profiling
forge test --gas-report -vvv

Trade-offs

Code Readability vs Gas

// More readable
if (condition1 && condition2) {
    doSomething();
}

// More gas efficient but less clear
if (condition1) {
    if (condition2) {
        doSomething();
    }
}

Recommendation: Prioritize readability unless gas savings are significant (>5%)

Deployment Cost vs Runtime Cost

// Cheaper deployment, expensive runtime
// Fewer optimizer runs (1-200)

// Expensive deployment, cheap runtime
// More optimizer runs (1000-10000)

Recommendation: Optimize for runtime if contract will be used frequently

Quick Reference

Technique Savings Risk Effort
Variable packing High Low Low
Cache storage reads High Low Low
Use constants High None Low
External over public Low None Low
Custom errors Medium None Low
Unchecked increments Low Medium Low
++i over i++ Low None Low
Calldata over memory Medium None Low
Batch operations High Low Medium
Assembly High High High

Remember: Security and correctness come first. Only optimize after ensuring your code is secure and functions correctly. Profile before optimizing to identify the most impactful changes.