Gas optimization techniques and patterns for efficient Solidity smart contracts. Use when optimizing contract gas costs or reviewing code for efficiency improvements.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill provides techniques, patterns, and best practices for optimizing gas costs in Solidity smart contracts.
Use this skill when:
Important: Always prioritize security over gas optimization. Only optimize after ensuring correctness and security.
| 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 |
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
// ❌ 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!
// ❌ 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;
}
// ❌ 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 storageimmutable: ~2,000 gas per read vs storage// ❌ 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;
}
}
// 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
// ❌ 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
// ❌ 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!
// ✅ Put cheap checks first
require(msg.sender == owner && expensiveCheck(), "Failed");
// Expensive check only runs if sender is owner
// ❌ 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
// ❌ 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
// ❌ 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
}
}
// ✅ 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
}
// ❌ 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 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)
// ❌ 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;
}
// ✅ Mappings: O(1) access, good for sparse data
mapping(uint256 => User) public users;
// ✅ Arrays: Good for iteration, dense data
User[] public userArray;
Trade-offs:
// ❌ 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)
// ❌ 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;
}
// ✅ 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!
// ❌ 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; }
}
}
// ❌ 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
// ❌ 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];
}
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)
}
}
};
// ✅ Use latest stable version for best optimizations
pragma solidity ^0.8.30;
Newer versions have better:
# Gas report in tests
forge test --gas-report
# Gas snapshot
forge snapshot
# Compare snapshots
forge snapshot --diff
// hardhat.config.js
require("hardhat-gas-reporter");
module.exports = {
gasReporter: {
enabled: true,
currency: "USD",
coinmarketcap: process.env.CMC_API_KEY
}
};
// ❌ 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+
// ❌ Very expensive
string memory result = string(abi.encodePacked(str1, str2, str3));
// ✅ Avoid strings in contracts when possible
// Use events or off-chain concatenation
// ❌ 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;
// ❌ 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
}
}
# Baseline gas report
forge test --gas-report > gas-before.txt
forge snapshot --snap baseline.snap
# Compare gas usage
forge test --gas-report > gas-after.txt
forge snapshot --diff baseline.snap
# Detailed gas profiling
forge test --gas-report -vvv
// 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%)
// 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
| 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.