Gas optimization techniques

Loading

Gas optimization is a crucial aspect of Ethereum and other blockchain-based smart contract development. Every operation on the blockchain costs gas, which users pay for using cryptocurrency (usually ETH). Minimizing gas costs leads to more efficient, cost-effective contracts and better scalability for decentralized applications (dApps). Poorly optimized smart contracts can result in higher user fees, failed transactions, or even the inability to deploy contracts due to block gas limits.

In this guide, we will explore the top gas optimization techniques, categorized step-by-step with practical Solidity examples.


1. Use uint256 over Smaller Integers in Storage

While using smaller data types like uint8 or uint16 seems efficient, the EVM (Ethereum Virtual Machine) processes uint256 natively. Using non-256-bit integers in storage can actually increase gas usage due to additional packing and unpacking operations.

// Good
uint256 public value;

// Avoid this (unless for tightly packed struct)
uint8 public smallValue;

2. Pack Structs and Storage Variables Efficiently

Storage is one of the most expensive operations. Variables are stored in 32-byte (256-bit) slots. Packing smaller variables together into a single slot reduces the number of storage slots used.

// Better struct packing
struct Packed {
uint128 a;
uint128 b;
}

// Avoid this (uses more storage slots)
struct Unpacked {
uint256 a;
uint256 b;
}

Place same-sized or smaller variables next to each other to allow Solidity to pack them into a single slot.


3. Use calldata Instead of memory for Function Arguments

When dealing with external functions that do not need to modify arguments, use calldata to reduce copying costs.

// Gas efficient
function processData(uint256[] calldata data) external {
// data is read-only and cheaper
}

// More expensive
function processData(uint256[] memory data) external {
// memory copies the array
}

4. Short-Circuit Conditions and Optimize require Statements

Place cheaper checks before expensive ones in conditionals to minimize gas in failed transactions.

// Good
require(msg.sender == owner && balance > 0, "Invalid");

// Avoid this
require(balance > 0 && msg.sender == owner, "Invalid"); // Evaluates both

Also, avoid complex require() messages unless necessary, as longer error strings use more gas.


5. Use immutable and constant Where Appropriate

  • constant: Used for compile-time constants (e.g., fixed numbers).
  • immutable: Used for values set once at construction and never changed.
uint256 public constant MAX_SUPPLY = 10000;
address public immutable owner;

constructor() {
owner = msg.sender;
}

Both constant and immutable variables save gas compared to regular storage variables.


6. Minimize Use of storage Writes

Writing to storage costs significantly more gas than reading from it. Avoid unnecessary writes and use memory or calldata when possible.

// Expensive
function update(uint256 newValue) public {
value = newValue; // Always writes
}

// Better
function update(uint256 newValue) public {
if (value != newValue) {
value = newValue; // Only writes if changed
}
}

7. Use unchecked for Arithmetic Where Safe (Solidity ≥0.8.0)

Solidity 0.8.0+ includes automatic overflow checks. If you’re certain an operation won’t overflow, disable these checks to save gas.

function increment(uint256 x) public pure returns (uint256) {
unchecked {
return x + 1;
}
}

Only use this when you’re 100% sure the operation is safe.


8. Minimize External Contract Calls

External calls cost gas and are also potential security risks (e.g., reentrancy). Minimize them, cache results when possible, and use internal logic when feasible.

// Costly
token.balanceOf(msg.sender);

// Better: cache if needed multiple times
uint256 balance = token.balanceOf(msg.sender);

9. Use for Loops Carefully

Avoid unbounded loops in functions, especially those triggered by users. These can cause transactions to run out of gas and revert.

// Gas-efficient loop (limited iterations)
function sum(uint256[] calldata nums) external pure returns (uint256) {
uint256 total;
uint256 length = nums.length;
for (uint256 i = 0; i < length; ++i) {
total += nums[i];
}
return total;
}

Always consider the maximum array length in loops.


10. Use Events Instead of Storage Logs

Use emit to log important data rather than storing it permanently if retrieval is not required by contracts later.

event ActionPerformed(address indexed user, uint256 amount);

function performAction() external {
emit ActionPerformed(msg.sender, 100);
}

Events are stored in transaction logs, which are cheaper than storage writes and useful for external monitoring tools.


11. Avoid Redundant State Variables

If a value can be computed from existing variables, avoid storing it. Instead, calculate it dynamically to save storage costs.

// Avoid redundant storage
uint256 public totalSupply = a + b;

// Better
function totalSupply() public view returns (uint256) {
return a + b;
}

12. Use Libraries for Reusable Logic

OpenZeppelin’s SafeMath, Counters, and custom utility libraries can reduce code duplication and contract size.

using Counters for Counters.Counter;
Counters.Counter private _tokenId;

function mint() public {
_tokenId.increment();
uint256 newId = _tokenId.current();
}

13. Delete Storage Variables When No Longer Needed

If temporary storage is no longer needed (like an auction record), explicitly delete it to get a gas refund.

delete auctionData[auctionId];

This refunds some gas based on the storage freed.


14. Return Minimal Data

Returning large arrays or data structures in public view functions increases gas for anyone calling them from a contract (though not for pure web3 reading). Keep outputs small where possible.


15. Avoid selfdestruct for Gas Refund

In Ethereum’s current roadmap, selfdestruct will likely be deprecated and no longer provide gas refunds. Avoid relying on it for optimization in future-proof code.

Leave a Reply

Your email address will not be published. Required fields are marked *