Skip to content
Logo

Gas Optimization

This guide covers profiling gas usage, creating gas snapshots, optimization techniques, and using the Solidity IR pipeline.

Gas snapshots

Create a baseline for gas usage:

$ forge snapshot

This generates .gas-snapshot with gas costs for each test:

CounterTest:test_Increment() (gas: 28334)
CounterTest:test_SetNumber() (gas: 27523)
TokenTest:testFuzz_Transfer(uint256) (gas: 42156)

Compare against the baseline:

$ forge snapshot --check

Diff against another snapshot:

$ forge snapshot --diff .gas-snapshot.old

Gas reports

Get detailed gas reports for contract functions:

$ forge test --gas-report

Output shows min, avg, median, and max gas for each function:

| Function    | min   | avg   | median | max   |
|-------------|-------|-------|--------|-------|
| transfer    | 51234 | 52341 | 51234  | 65123 |
| approve     | 24521 | 24521 | 24521  | 24521 |
| balanceOf   | 562   | 562   | 562    | 562   |

Profiling specific tests

Focus on gas-critical functions:

$ forge test --match-test test_Transfer --gas-report -vvv

Optimization techniques

Storage optimization

Storage is the most expensive operation. Minimize storage writes:

struct User {
    uint128 balance;    // 16 bytes
    uint64 lastUpdate;  // 8 bytes
    uint64 rewards;     // 8 bytes
}

Caching storage reads

function process() external {
    uint256 _balance = balance; 
    
    for (uint256 i; i < 100; i++) {
        _balance += i;
    }
    
    balance = _balance; 
}

Unchecked arithmetic

When overflow is impossible, skip checks:

function sum(uint256[] calldata values) external pure returns (uint256 total) {
    for (uint256 i; i < values.length;) {
        unchecked { 
            total += values[i];
            ++i;
        } 
    }
}

Calldata vs memory

Use calldata for read-only array parameters:

function process(uint256[] calldata values) external pure returns (uint256) { 
    // calldata is cheaper - no copy needed
}

Short-circuit evaluation

Order conditions by likelihood and cost:

// Cheap check first, expensive check second
if (amount > 0 && token.balanceOf(msg.sender) >= amount) {
    // ...
}

Custom errors vs require strings

error InsufficientBalance(uint256 available, uint256 required);
 
function transfer(uint256 amount) external {
    if (balances[msg.sender] < amount) {
        revert InsufficientBalance(balances[msg.sender], amount);
    }
}

IR pipeline optimization

The Yul IR pipeline can produce more optimized bytecode:

foundry.toml
[profile.default]
via_ir = true
optimizer = true
optimizer_runs = 200

Or enable for production builds only:

foundry.toml
[profile.default]
via_ir = false
 
[profile.production]
via_ir = true
optimizer = true
optimizer_runs = 10000
$ FOUNDRY_PROFILE=production forge build

IR pipeline trade-offs

SettingCompilation timeGas efficiencyCode size
via_ir = falseFastGoodLarger
via_ir = trueSlowBetterSmaller
High optimizer runsSlowerBest for frequent callsLarger
Low optimizer runsFasterBest for deploymentSmaller

Optimizer runs tuning

optimizer_runs balances deployment cost vs runtime cost:

RunsBest for
1Deploy once, rarely called
200Default, balanced
10,000+Frequently called contracts
foundry.toml
[profile.default]
optimizer = true
optimizer_runs = 200
 
# For frequently-called core contracts
[profile.default.optimizer_details.yulDetails]
optimizerSteps = "dhfoDgvulfnTUtnIf"

Assembly optimization

For critical paths, inline assembly can save gas:

// Get balance without function call overhead
function getBalance(address account) internal view returns (uint256 bal) {
    assembly {
        bal := balance(account)
    }
}
 
// Efficient ownership check
function isOwner(address account) internal view returns (bool result) {
    address _owner = owner;
    assembly {
        result := eq(account, _owner)
    }
}

Gas benchmarking in tests

Create dedicated gas benchmarks:

test/Gas.t.sol
contract GasTest is Test {
    Token token;
 
    function setUp() public {
        token = new Token();
        token.mint(address(this), 1000 ether);
    }
 
    function test_Transfer_Gas() public {
        uint256 gasBefore = gasleft();
        token.transfer(address(1), 100);
        uint256 gasUsed = gasBefore - gasleft();
        
        emit log_named_uint("transfer gas", gasUsed);
        assertLt(gasUsed, 60000, "transfer too expensive");
    }
}

CI integration

Fail CI if gas regresses:

.github/workflows/gas.yml
- name: Check gas snapshots
  run: |
    forge snapshot --check --tolerance 5

The --tolerance flag allows small regressions (5% in this case).

Best practices

PracticeDescription
Baseline firstCreate snapshots before optimizing
Measure, don't guessUse gas reports to find actual bottlenecks
Test edge casesGas can vary significantly with input size
Cache in productionUse via_ir only for final builds
Document optimizationsExplain non-obvious gas optimizations