Gas Optimization skill for Codex

Cut storage costs first, prove every saving. Codex baselines with forge snapshot and gas reports, ranks functions by cost times call volume, and applies one change at a time, starting with storage packing, caching and immutables rather than syntax tricks. Each saving is kept only if tests pass and the snapshot proves it, and a bundled script diffs two snapshots with a regression threshold for CI.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- gas-optimization

Installs to ~/.agents/skills/gas-optimization/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $gas-optimization, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Reduces Solidity gas costs by measurement - forge snapshot and gas reports before and after, storage packing, immutable and constant, caching SLOADs, calldata over memory, custom errors, transient storage, bitmaps, events instead of storage, optimizer runs and via-IR trade-offs - without trading away safety or readability for noise-level savings. Use when asked to make a contract cheaper to deploy or call, review a gas report, cut costs for a hot function, tune optimizer settings, or gate gas regressions in CI.

Use it when

  • A hot contract function costs users too much gas
  • A gas report or snapshot diff needs interpretation
  • Optimizer runs, via-IR or evm_version settings need choosing
  • You want gas regressions caught in pull requests

Use something else when

How to check the result

Snapshots before and after show the per-function delta, the full test suite passes, and storage layout output is unchanged for upgradeable contracts or the change is documented.

The mistake it prevents

Spending effort on micro-syntax like ++i or != 0 while a loop still reads the same storage slot on every iteration.

~/.agents/skills/gas-optimization/SKILL.md

--- name: gas-optimization description: Reduces Solidity gas costs by measurement - forge snapshot and gas reports before and after, storage packing, immutable and constant, caching SLOADs,… license: MIT ---

An optimization counts only when a snapshot proves it, the tests still pass, and the saving matters at the function’s real call volume. Storage access dominates almost every bill; fix that before touching syntax. Never weaken a check, remove an event users rely on, or add assembly to save a few hundred gas on a function called once.

Workflow

- [ ] 1. Read AGENTS.md, foundry.toml / hardhat config (optimizer, runs, via_ir, evm_version)
- [ ] 2. Make sure tests exercise the hot paths with realistic state (warm vs cold matters)
- [ ] 3. Baseline: forge snapshot (commit it) and forge test --gas-report
- [ ] 4. Rank functions by cost x expected call volume; pick the top few
- [ ] 5. Apply one change at a time from the table below; re-snapshot after each
- [ ] 6. Keep a change only if tests pass and the saving is real; revert noise
- [ ] 7. Report before/after per function with the reasoning
forge snapshot                          # writes .gas-snapshot
forge test --gas-report                 # per-function min/avg/median/max, deploy cost
forge snapshot --diff .gas-snapshot     # compare current run with the saved file
forge snapshot --check                  # non-zero exit if anything changed (CI gate)
forge build --sizes                     # runtime and initcode size vs limits

For a readable diff between two saved snapshots with a regression threshold, run python3 scripts/gas_diff.py before.snap after.snap --threshold 1 (exit 1 when any test regresses more than the threshold). Save the baseline with git show main:.gas-snapshot > before.snap.

In Hardhat projects use hardhat-gas-reporter (bundled in the Hardhat 2 toolbox) or run the Solidity tests through Foundry for snapshots.

Cost model to reason with

OperationApprox. gasNote
SSTORE zero to non-zero22,100 coldThe single most expensive common op
SSTORE non-zero to non-zero5,000 cold / 2,900 warm
SLOAD2,100 cold / 100 warmCold = first access in the transaction
TSTORE / TLOAD (EIP-1153)100Cleared at end of transaction
Access a cold external address2,600CALL, BALANCE, EXTCODESIZE
LOG with one topic375 + 375 + 8 per byteFar cheaper than storage for history
Calldata byte16 non-zero / 4 zeroOn rollups, L1 data cost dominates instead
Storage refundcapped at 1/5 of tx gasClearing storage is not free money

Changes ranked by typical payoff

ChangeWhen it paysWatch out
Pack storage: order fields so small types share a 32-byte slotStructs and state read or written togetherOnly helps when packed fields are accessed together
constant / immutable for values fixed at compile time or deployAlwaysImmutables cannot be changed by upgrades of the same proxy implementation
Cache storage in a local variable inside a functionA slot read more than onceWrite back once at the end
Avoid writing unchanged values; avoid zero-to-nonzero flipsFlags, counters that resetKeep a non-zero sentinel (1/2 instead of 0/1) where it saves a 22k write
Bitmaps (BitMaps, uint256 masks) for booleansClaimed flags, allowlistsReadability; document bit layout
Events instead of storage for data never read on-chainHistory, receiptsOff-chain consumers must index logs
Mappings instead of arrays for lookupsMembership checksLose iteration; keep a count if needed
calldata for external array and string paramsRead-only inputsCannot modify calldata
Custom errors instead of revert stringsEvery revert pathAlready the default in modern code
Transient storage for reentrancy locks and per-tx flagsChains supporting CancunValue must not need to survive the transaction
Merkle proofs instead of on-chain allowlistsLarge allowlistsUsers supply proofs
Minimal proxies (ERC-1167 clones) for many identical contractsFactoriesExtra DELEGATECALL per call (about 2.6k cold)
Batch operations (multicall, batch transfer)Users doing many small callsBound batch size

Packing example

// Before: 3 slots. owner (slot 0), amount (slot 1), openedAt + closed (slot 2)
struct Position { address owner; uint256 amount; uint64 openedAt; bool closed; }

// After: 2 slots. amount (slot 0), owner + openedAt + closed = 29 bytes (slot 1)
struct Position { uint256 amount; address owner; uint64 openedAt; bool closed; }

A uint256 between small fields forces each small field into its own slot. Verify the layout instead of guessing:

forge inspect Vault storageLayout

Pick widths that cannot overflow in the protocol’s lifetime (uint64 seconds lasts beyond any deployment; uint96 amounts cap at about 7.9e28 wei) and cast with SafeCast.

Caching example

// Before: reads `totalStaked` and `rewardRate` from storage on every iteration
for (uint256 i; i < users.length; i++) {
    rewards[users[i]] += stakes[users[i]] * rewardRate / totalStaked;
}

// After: two SLOADs total, calldata array
function distribute(address[] calldata users) external onlyKeeper {
    uint256 rate = rewardRate;
    uint256 total = totalStaked;
    uint256 n = users.length;
    for (uint256 i; i < n; ++i) {
        address u = users[i];
        rewards[u] += stakes[u] * rate / total;
    }
}

Compiler settings

SettingGuidance
optimizer_runsLow (200 or less) favours deploy size; high (10,000+) favours runtime for hot contracts. Measure both with --gas-report
via_ir = trueOften cheaper runtime and fixes stack-too-deep; slower compiles. Re-run the full suite and diff bytecode size after switching
evm_versionNewer versions enable cheaper opcodes (PUSH0, MCOPY, TSTORE) but must be supported on every target chain
Solidity version0.8.22+ removes overflow checks on simple for loop increments automatically; manual unchecked { ++i; } is no longer needed there

Changing optimizer settings on an already-deployed upgradeable contract changes bytecode but not storage; still re-run the full suite and storage-layout checks.

Micro-optimizations: usually skip

!= 0 vs > 0, ++i vs i++, external vs public, and short-circuit ordering save single-digit to tens of gas with modern solc. Apply them only in loops that run many times, and only when a snapshot shows the difference. Inline assembly is a last resort: it removes compiler safety checks, needs a comment explaining the memory and stack assumptions, and must have dedicated fuzz tests.

L2 considerations

On rollups, execution gas is cheap and the L1 data fee is often the larger part of a user’s cost. Measure on the target chain (or its fork with the right chainType) before optimizing execution. Reducing calldata (tighter argument types, packed encodings) matters more there than saving SLOADs.

Hard rules

  • Never remove access control, input validation, reentrancy guards or events to save gas.
  • Never use unchecked without a comment proving the bound.
  • Each optimization is its own commit with its snapshot delta in the message.
  • A gas regression gate in CI uses forge snapshot --check or the diff script with a tolerance; do not make every one-gas change a failure.

Report template

**Baseline:** main @ abc123, solc 0.8.28, optimizer 200 runs
| Function | Before | After | Delta | Change |
|---|---|---|---|---|
| deposit | 71,240 | 52,110 | -26.9% | packed Position, cached rewardRate |
| claim | 48,900 | 46,700 | -4.5% | bitmap for claimed flags |
**Deploy cost:** 1,412,000 -> 1,380,500
**Rejected:** via_ir (runtime -1%, compile time x4; not worth it here)
**Tests:** all pass; storage layout unchanged / changed (non-upgradeable)

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Gas Optimization"
  short_description: "Cut storage costs first, prove every saving"
  default_prompt: "$gas-optimization Make deposit and claim in this contract cheaper without changing behaviour"
scripts/gas_diff.py103 lines, 3.5 KB
#!/usr/bin/env python3
"""Compare two Foundry .gas-snapshot files and flag regressions.

Usage:
    python3 gas_diff.py BEFORE AFTER [--threshold PCT] [--all]

BEFORE and AFTER are .gas-snapshot files (for example, one saved from main with
`git show main:.gas-snapshot > /tmp/before.snap`). For fuzz tests the median (~)
is compared; for plain tests the single gas value. Invariant entries carry no
gas figure and are skipped.

Exit codes: 0 = no test regressed by more than the threshold, 1 = at least one
regression above the threshold, 2 = usage or parse error.
"""
from __future__ import annotations

import argparse
import re
import sys

LINE = re.compile(r"^(?P<name>\S+\(.*?\))\s+\((?P<body>.*)\)\s*$")
GAS = re.compile(r"gas:\s*(\d+)")
MEDIAN = re.compile(r"~:\s*(\d+)")


def parse(path: str) -> dict[str, int]:
    out: dict[str, int] = {}
    try:
        with open(path, encoding="utf-8") as fh:
            for raw in fh:
                line = raw.strip()
                if not line:
                    continue
                m = LINE.match(line)
                if not m:
                    continue
                body = m.group("body")
                g = GAS.search(body) or MEDIAN.search(body)
                if g:
                    out[m.group("name")] = int(g.group(1))
    except OSError as exc:
        print(f"error: cannot read {path}: {exc}", file=sys.stderr)
        sys.exit(2)
    return out


def main() -> int:
    ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
    ap.add_argument("before")
    ap.add_argument("after")
    ap.add_argument("--threshold", type=float, default=1.0,
                    help="percent increase that counts as a regression (default 1.0)")
    ap.add_argument("--all", action="store_true", help="also list unchanged tests")
    args = ap.parse_args()

    before, after = parse(args.before), parse(args.after)
    if not before or not after:
        print("error: one of the snapshots has no parseable gas entries", file=sys.stderr)
        return 2

    rows = []
    for name in sorted(set(before) | set(after)):
        b, a = before.get(name), after.get(name)
        if b is None:
            rows.append((name, "-", a, "", "new"))
            continue
        if a is None:
            rows.append((name, b, "-", "", "removed"))
            continue
        delta = a - b
        pct = (delta / b * 100) if b else 0.0
        if pct > args.threshold:
            status = "REGRESSION"
        elif delta < 0:
            status = "improved"
        elif delta > 0:
            status = "up (within threshold)"
        else:
            status = "same"
        if status == "same" and not args.all:
            continue
        rows.append((name, b, a, f"{delta:+d} ({pct:+.2f}%)", status))

    regressions = [r for r in rows if r[4] == "REGRESSION"]
    total_b = sum(before[n] for n in before if n in after)
    total_a = sum(after[n] for n in after if n in before)

    if rows:
        w = max(len(r[0]) for r in rows)
        print(f"{'test'.ljust(w)}  {'before':>10}  {'after':>10}  {'delta':>20}  status")
        for name, b, a, d, s in rows:
            print(f"{name.ljust(w)}  {str(b):>10}  {str(a):>10}  {d:>20}  {s}")
    else:
        print("no changes")
    if total_b:
        print(f"\nshared tests total: {total_b} -> {total_a} "
              f"({(total_a - total_b) / total_b * 100:+.2f}%)")
    print(f"regressions above {args.threshold}%: {len(regressions)}")
    return 1 if regressions else 0


if __name__ == "__main__":
    sys.exit(main())

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem