May 8, 2026·9 min·By

Solidity Gas Optimization: Cut Your Transaction Costs by 50%

Soliditygas optimizationEthereumWeb3smart contracts

Gas costs money. On Ethereum mainnet, a complex transaction can cost $50-200. Here's how to cut that in half.

Understand What Costs Gas

SSTORE (storage write): 20,000 gas (cold), 2,900 gas (warm)
SLOAD (storage read):   2,100 gas (cold), 100 gas (warm)
MSTORE (memory write):  3 gas + expansion
CALLDATALOAD:           3 gas
ADD, MUL, etc:          3-8 gas

Storage operations dominate gas costs. Minimize them.

1. Pack Struct Variables

EVM storage uses 32-byte slots. Pack multiple small variables into one slot:

// Bad: 3 separate storage slots = 3x SLOAD/SSTORE cost
struct BadUser {
    uint256 id;       // slot 0 (32 bytes)
    bool active;      // slot 1 (32 bytes wasted on 1 bit)
    uint256 balance;  // slot 2 (32 bytes)
}

// Good: 2 storage slots
struct GoodUser {
    uint256 id;       // slot 0
    uint128 balance;  // slot 1 (16 bytes)
    uint64 lastSeen;  // slot 1 (8 bytes, shares with balance)
    bool active;      // slot 1 (1 byte, shares with above)
}

Order matters: group smaller types together so they fit in the same slot.

2. Use calldata Instead of memory

// Bad: copies array to memory, costs gas
function processItems(uint256[] memory items) external {
    for (uint i = 0; i < items.length; i++) { /* ... */ }
}

// Good: reads directly from calldata, much cheaper
function processItems(uint256[] calldata items) external {
    for (uint i = 0; i < items.length; i++) { /* ... */ }
}

Use calldata for function parameters you don't modify. Use memory only when you need to modify the data.

3. Cache Storage Variables

// Bad: 3 SLOAD operations
function bad() external {
    for (uint i = 0; i < arr.length; i++) {
        total += arr[i];  // arr.length SLOAD each iteration!
    }
}

// Good: 1 SLOAD
function good() external {
    uint256[] memory _arr = arr;  // single SLOAD
    uint256 len = _arr.length;   // cache length
    uint256 _total = 0;          // local variable (memory)

    for (uint i = 0; i < len; i++) {
        _total += _arr[i];
    }
    total = _total;  // single SSTORE at the end
}

4. Use Custom Errors Instead of Strings

// Bad: stores and returns the string on revert (expensive)
require(msg.sender == owner, "Ownable: caller is not the owner");

// Good: just a 4-byte selector on revert
error NotOwner();
if (msg.sender != owner) revert NotOwner();

Custom errors save ~50 gas per revert compared to string errors.

5. Short-Circuit Expensive Operations

// Cheaper check first (avoid SLOAD if msg.value check fails)
function deposit() external payable {
    require(msg.value >= MIN_DEPOSIT, "Too small");  // cheap
    require(whitelisted[msg.sender], "Not whitelisted");  // expensive SLOAD
}

6. Avoid Redundant Storage Writes

// Wasteful: writing same value back
function update(uint256 newValue) external {
    if (storedValue != newValue) {  // Check before write
        storedValue = newValue;     // Only write if changed
    }
}

SSTORE to a non-zero value costs 5,000 gas. SSTORE to zero costs 2,900 but gets a 4,800 gas refund. SSTORE of the same value costs only 100 gas (warm slot, no change).

7. Mappings vs Arrays

// Array: O(n) deletion, re-indexing needed
uint256[] public values;

// Mapping: O(1) lookup and deletion
mapping(uint256 => bool) public hasValue;

For existence checks, use a mapping. Arrays only when you need iteration.

8. Use Immutable and Constant

// constant: computed at compile time, 0 gas to read
uint256 constant MAX_SUPPLY = 10_000;

// immutable: set once in constructor, reads cost ~200 gas less than storage reads
address immutable owner;

constructor() {
    owner = msg.sender;  // Set once, then cheap to read
}

9. Optimizer Settings

// hardhat.config.js
module.exports = {
    solidity: {
        version: "0.8.24",
        settings: {
            optimizer: {
                enabled: true,
                runs: 200  // Higher = optimize for repeated calls, larger bytecode
                           // Lower = optimize for one-time deployment cost
            }
        }
    }
}

Use runs: 200 for contracts called frequently. Use runs: 1 for factory contracts deployed rarely.

Measure Everything

# Hardhat gas reporter
npm install hardhat-gas-reporter

# In hardhat.config.js
gasReporter: { enabled: true, currency: 'USD' }

Never optimize without measuring first. Profile with gas reporter, optimize the highest-cost functions, and verify savings with before/after tests.

K
Founder & Technical Lead, Innovibe

Building software for 15+ years. Passionate about AI, system design, and shipping things that work.

Frequently asked questions

Does Innovibe build this kind of thing for clients?+

Yes — this is exactly what we do day-to-day for clients across BC and Canada. If you'd rather have us build and maintain it than implement it yourself, reach out.

How do I decide whether to build this in-house or hire an agency?+

Build in-house if your team has the skills and bandwidth and this is core to your product. Hire out if it's infrastructure, if speed matters, or if the expertise gap would take months to close. We're biased, obviously — but we'll tell you honestly when in-house makes more sense.

What tech stack does Innovibe use for projects like this?+

Next.js + TypeScript on the frontend, Node.js or Go on the backend, Postgres for the primary data store, and GCP (Cloud Run, BigQuery, Pub/Sub) for infrastructure. We pick tools that are boring in the best way — proven, well-documented, and easy to hire for.

Building something with AI?

We scope and ship AI features quickly. Let's talk.

Start a Conversation