Lesson 18 of 25
Gas Optimization
Patterns and techniques to dramatically reduce gas consumption in your contracts.
AdvancedPacking Storage Variables
The EVM stores data in 32-byte slots. If multiple variables fit in one slot, they share it — saving thousands of gas. Pack small types together.
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// ❌ Wasteful — each variable occupies a full 32-byte slot = 3 slots
contract Unpacked {
uint256 a; // slot 0
uint8 b; // slot 1 (wastes 31 bytes!)
uint256 c; // slot 2
}
// ✅ Optimized — b and d share a slot with a smaller variable = 2 slots
contract Packed {
uint128 a; // slot 0 (bytes 0-15)
uint64 b; // slot 0 (bytes 16-23)
uint64 c; // slot 0 (bytes 24-31)
uint256 d; // slot 1
}
Use calldata for External Functions
For external functions that read but don't modify array/string parameters, use
calldata instead of memory.
Solidity
// ❌ Copies data into memory — extra gas
function processMemory(uint256[] memory data) external pure
returns (uint256 sum)
{
for (uint i = 0; i < data.length; i++) sum += data[i];
}
// ✅ Reads directly from calldata — no copy
function processCalldata(uint256[] calldata data) external pure
returns (uint256 sum)
{
for (uint i = 0; i < data.length; i++) sum += data[i];
}
Custom Errors over String Messages
Custom errors (0.8.4+) are dramatically cheaper than string revert messages. The string is NOT stored on-chain at all — just a 4-byte selector.
Solidity
// ❌ String stored as calldata — expensive
function old() public {
require(msg.sender == owner, "Ownable: caller is not the owner");
}
// ✅ Custom error — just a 4-byte selector
error NotOwner();
function modern() public {
if (msg.sender != owner) revert NotOwner();
}
// Gas saved: ~200 gas per call, more with longer strings
Short-Circuit Evaluation & Loop Tips
Arrange conditions from cheapest to most expensive. Cache storage reads. Avoid dynamic-length loops.
Solidity
// ❌ Expensive: storage read happens even if first check fails
function bad(address user) public view returns (bool) {
return isAdmin[user] || msg.sender == owner; // storage first
}
// ✅ Cheap: msg.sender is free, storage read only if needed
function good(address user) public view returns (bool) {
return msg.sender == owner || isAdmin[user];
}
// Cache storage length outside loop
function sumOptimized(uint256[] storage arr) internal view
returns (uint256 total)
{
uint256 len = arr.length; // read once
for (uint256 i; i < len;) {
total += arr[i];
unchecked { ++i; } // skip overflow check — safe here
}
}