Gas Optimization Skill
Advanced gas optimization techniques for EVM smart contracts with benchmark-driven analysis.
Capabilities
- Storage Optimization: Storage packing, slot management, SLOAD/SSTORE minimization
- Memory Management: Memory vs calldata optimization, expansion costs
- Assembly/Yul: Low-level optimization for critical paths
- Data Structures: Gas-efficient mappings, arrays, and structs
- Batch Operations: Multi-call patterns, bulk transfers
- Benchmarking: Gas profiling, comparison analysis
MCP/Tool Integration
| Tool | Purpose | Reference | |------|---------|-----------| | Foundry MCP | Gas reports, testing | foundry-mcp-server | | EVM MCP Tools | Opcode analysis | evm-mcp-tools |
Storage Optimization
Storage Layout Packing
// BAD: 3 storage slots (96 bytes used, 96 bytes allocated)
contract BadPacking {
uint128 a; // slot 0 (16 bytes)
uint256 b; // slot 1 (32 bytes) - can't pack with a
uint128 c; // slot 2 (16 bytes)
}
// GOOD: 2 storage slots (80 bytes used, 64 bytes allocated)
contract GoodPacking {
uint128 a; // slot 0, bytes 0-15
uint128 c; // slot 0, bytes 16-31
uint256 b; // slot 1
}
// Gas savings: ~20,000 gas per SSTORE avoided
Minimize Storage Writes
// BAD: Multiple storage writes
function badUpdate(uint256 newA, uint256 newB) external {
a = newA; // SSTORE: 20,000 gas (cold) or 2,900 gas (warm)
b = newB; // SSTORE: 2,900 gas (warm slot in same tx)
}
// GOOD: Single storage write with packed struct
struct Data {
uint128 a;
uint128 b;
}
Data public data;
function goodUpdate(uint128 newA, uint128 newB) external {
data = Data(newA, newB); // Single SSTORE: 20,000 gas
}
Use Mappings Over Arrays for Lookups
// BAD: O(n) lookup, expensive for large arrays
uint256[] public values;
function exists(uint256 value) public view returns (bool) {
for (uint i = 0; i < values.length; i++) {
if (values[i] == value) return true; // SLOAD per iteration
}
return false;
}
// GOOD: O(1) lookup
mapping(uint256 => bool) public valueExists;
function exists(uint256 value) public view returns (bool) {
return valueExists[value]; // Single SLOAD
}
Memory vs Calldata
Function Parameters
// BAD: Copies array to memory
function processArray(uint256[] memory data) external {
// Memory copy cost: 3 gas per word + expansion
}
// GOOD: Read directly from calldata
function processArray(uint256[] calldata data) external {
// No copy, just pointer to calldata
// Savings: ~60 gas per 32 bytes
}
// Note: Use memory if you need to modify the array
String/Bytes Handling
// For read-only operations, use calldata
function validate(string calldata input) external pure returns (bool) {
return bytes(input).length > 0;
}
// For modifications, use memory
function transform(string memory input) internal pure returns (string memory) {
bytes memory b = bytes(input);
b[0] = 'X';
return string(b);
}
Unchecked Arithmetic
// BAD: Overflow checks on every operation (Solidity 0.8+)
function sumArray(uint256[] calldata arr) external pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < arr.length; i++) {
sum += arr[i]; // Overflow check: ~40 gas per operation
}
return sum;
}
// GOOD: Unchecked when overflow is impossible
function sumArray(uint256[] calldata arr) external pure returns (uint256) {
uint256 sum = 0;
uint256 length = arr.length;
for (uint256 i = 0; i < length;) {
unchecked {
sum += arr[i];
++i; // ++i is cheaper than i++
}
}
return sum;
}
// Savings: ~40 gas per iteration
Loop Optimizations
Cache Array Length
// BAD: Length read from storage each iteration
for (uint i = 0; i < array.length; i++) { } // SLOAD per iteration
// GOOD: Cache length
uint256 length = array.length;
for (uint i = 0; i < length; i++) { } // Single SLOAD
Pre-increment
// BAD: Post-increment creates temporary
for (uint i = 0; i < length; i++) { }
// GOOD: Pre-increment is cheaper
for (uint i = 0; i < length; ++i) { }
// Savings: ~5 gas per iteration
Custom Errors
// BAD: String error messages
require(balance >= amount, "Insufficient balance");
// Cost: ~50 gas per character + memory expansion
// GOOD: Custom errors (Solidity 0.8.4+)
error InsufficientBalance(uint256 available, uint256 required);
if (balance < amount) revert InsufficientBalance(balance, amount);
// Cost: Fixed ~24 gas for error selector
// Savings: ~50+ gas for typical error messages
Assembly/Yul Optimization
Efficient Balance Check
// Solidity
function getBalance(address account) external view returns (uint256) {
return account.balance;
}
// Assembly (slightly cheaper)
function getBalance(address account) external view returns (uint256 bal) {
assembly {
bal := balance(account)
}
}
Efficient Memory Operations
// Copy 32 bytes efficiently
function copy32(bytes32 source) internal pure returns (bytes32 dest) {
assembly {
dest := source
}
}
// Efficient keccak256
function efficientHash(bytes32 a, bytes32 b) internal pure returns (bytes32 result) {
assembly {
mstore(0x00, a)
mstore(0x20, b)
result := keccak256(0x00, 0x40)
}
}
Batch Operations
Batch Transfers
// BAD: Individual transfers
function transferToMany(address[] calldata recipients, uint256 amount) external {
for (uint i = 0; i < recipients.length; ++i) {
token.transfer(recipients[i], amount); // 21000 base + transfer cost
}
}
// GOOD: Batch transfer (if supported)
function batchTransfer(
address[] calldata recipients,
uint256[] calldata amounts
) external {
// Single function call overhead
// Reduced SLOAD for token state
}
Multicall Pattern
function multicall(bytes[] calldata data) external returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; ++i) {
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
require(success);
results[i] = result;
}
}
// Combines multiple operations in single transaction
Benchmarking Workflow
Using Foundry Gas Reports
# Run tests with gas report
forge test --gas-report
# Snapshot gas usage
forge snapshot
# Compare against previous snapshot
forge snapshot --check
Gas Snapshot Format
| Contract | Function | Min | Avg | Max | # Calls |
|----------|----------|-----|-----|-----|---------|
| Token | transfer | 51234 | 54123 | 65432 | 100 |
| Token | approve | 24356 | 24356 | 24356 | 50 |
Comparison Testing
contract GasComparison is Test {
function test_gasComparison_approach1() public {
uint256 gasBefore = gasleft();
// Approach 1
uint256 gasUsed = gasBefore - gasleft();
emit log_named_uint("Approach 1 gas", gasUsed);
}
function test_gasComparison_approach2() public {
uint256 gasBefore = gasleft();
// Approach 2
uint256 gasUsed = gasBefore - gasleft();
emit log_named_uint("Approach 2 gas", gasUsed);
}
}
Common Optimizations Summary
| Technique | Savings | Risk | |-----------|---------|------| | Storage packing | 20,000 gas/slot | Low | | Calldata vs memory | 60 gas/32 bytes | Low | | Unchecked arithmetic | 40 gas/op | Medium | | Custom errors | 50+ gas/error | Low | | Cache storage reads | 100-2100 gas | Low | | Loop pre-increment | 5 gas/iteration | Low | | Assembly | Varies | High |
Process Integration
This skill integrates with:
gas-optimization.js- Full optimization processsmart-contract-development-lifecycle.js- Development best practicesamm-pool-development.js- DeFi-specific optimizations
Tools Reference
| Tool | Purpose | URL | |------|---------|-----| | Foundry | Gas reporting | foundry-rs | | Hardhat Gas Reporter | Gas reports | hardhat-gas-reporter | | evm.codes | Opcode costs | evm.codes | | Solidity Optimizer | Compiler optimization | Solidity Docs |
See Also
skills/evm-analysis/SKILL.md- Bytecode analysisagents/gas-optimizer/AGENT.md- Gas optimization agentreferences.md- Gas optimization resources