Gas optimization is crucial for smart contract development. Optimized contracts save users money, improve UX, and make your dApp more competitive. Let's explore proven strategies to reduce gas consumption.
Understanding Gas
Gas Costs
- 21,000 gas - Base transaction cost
- 2,000 gas - Storage slot (sstore)
- 5,000 gas - Event logging
- 20 gas - Non-zero value in calldata
- 4 gas - Zero value in calldata
Gas Price Formula
solidity
Total Cost = Gas Used × Gas Price + Base Fee + Priority FeeStorage Optimization
Use Calldata Instead of Memory
solidity
// Expensive - memory allocation
function process(string memory data) public {}
// Cheaper - calldata reference
function process(string calldata data) public {}Pack Variables
solidity
// Wastes storage slot
struct Bad {
uint128 a; // 16 bytes
uint128 b; // 16 bytes
uint256 c; // 32 bytes - new slot
}
// Optimized
struct Good {
uint256 a; // 32 bytes
uint128 b; // 16 bytes
uint128 c; // 16 bytes
}Use uint256 by Default
solidity
// More expensive
contract Expensive {
uint8 public counter; // Uses full 32-byte slot
}
// Cheaper
contract Cheaper {
uint256 public counter; // Same slot, more efficient
}Memory Optimization
Short-Circuit Evaluation
solidity
// Expensive
if (expensiveCheck() || cheapCheck()) {}
// Cheaper
if (cheapCheck() || expensiveCheck()) {}Batch Operations
solidity
// Expensive - multiple calls
function setValues(uint256[] memory values) public {
for (uint256 i = 0; i < values.length; i++) {
values[i] = i;
}
}
// Cheaper - batch
function setValues(uint256[] calldata values) public {
require(values.length <= 100);
// Process in batch
}Loop Optimization
Avoid Unbounded Loops
solidity
// Dangerous - gas limit risk
function processAll() public {
for (uint256 i = 0; i < items.length; i++) {
// process
}
}
// Safe - limit iterations
function processBatch(uint256 batchSize) public {
require(batchSize <= 100);
for (uint256 i = 0; i < batchSize; i++) {
require(i < items.length);
// process
}
}Cache Array Length
solidity
// Recalculates length each iteration
for (uint256 i = 0; i < array.length; i++) {}
// Caches length
uint256 length = array.length;
for (uint256 i = 0; i < length; i++) {}Function Optimization
Use Custom Errors
solidity
// Expensive - string storage
error InvalidAmount();
// Cheaper
require(amount > 0, "Amount must be greater than 0");
// Even cheaper
require(amount > 0);Optimize View Functions
solidity
// Expensive
function getBalance(address user) public view returns (uint256) {
return balances[user];
}
// Cheaper with caching
mapping(address => uint256) public balances;Event Logging
Efficient Events
solidity
// Expensive
event Log(string message, uint256 value1, uint256 value2);
// Cheaper
event Log(uint256 indexed value1, uint256 value2);
// Minimal
event Log(uint256 value);Indexed Parameters
solidity
// Up to 3 indexed parameters
event Transfer(
address indexed from,
address indexed to,
uint256 indexed tokenId,
uint256 value
);External Call Optimization
Low-Level Calls
solidity
// Expensive - call creates new memory frame
(bool success, ) = recipient.call{value: amount}("");
// Cheaper - delegatecall
(bool success, ) = recipient.delegatecall(abi.encodeWithSignature("transfer(address,uint256)", to, amount));Batch External Calls
solidity
// Multicall pattern
function multicall(bytes[] calldata data) external {
for (uint256 i = 0; i < data.length; i++) {
(bool success, ) = address(this).delegatecall(data[i]);
require(success, "Call failed");
}
}Design Patterns
Pull Over Push
solidity
// Expensive - multiple transfers
function distribute() public {
for (uint256 i = 0; i < recipients.length; i++) {
payable(recipients[i]).transfer(amount);
}
}
// Cheaper - pull pattern
function claim() public {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}Lazy Evaluation
solidity
// Calculate only when needed
uint256 private cachedResult;
bool private cached = false;
function expensiveCalculation() public returns (uint256) {
if (cached) return cachedResult;
uint256 result = /* complex calculation */;
cachedResult = result;
cached = true;
return result;
}Assembly Optimization
Yul for Gas Savings
solidity
function yulAdd(uint256 a, uint256 b) public pure returns (uint256) {
assembly {
let result := add(a, b)
mstore(0x00, result)
return(0x00, 0x20)
}
}Memory Operations
solidity
function bytesToBytes32(bytes memory data) public pure returns (bytes32) {
assembly {
result := mload(add(data, 32))
}
}Compiler Optimizations
Enable Optimizer
javascript
// hardhat.config.js
module.exports = {
solidity: {
version: "0.8.0",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
};Testing Gas Costs
Hardhat Gas Reporter
javascript
require("hardhat-gas-reporter");
module.exports = {
gasReporter: {
enabled: true,
currency: "USD",
gasPrice: 20
}
};Gas Snapshots
javascript
it("Should match gas snapshot", async function () {
const tx = await contract.function();
expect(tx).to.have.gasCost(50000);
});Common Gas Pitfalls
Storage in Loops
solidity
// VERY EXPENSIVE
for (uint256 i = 0; i < 100; i++) {
balances[i] = i; // 100 sstore operations
}
// Much cheaper
uint256[] memory temp = new uint256[](100);
for (uint256 i = 0; i < 100; i++) {
temp[i] = i;
}
for (uint256 i = 0; i < 100; i++) {
balances[i] = temp[i];
}Deleting Large Arrays
solidity
// Expensive
function clear() public {
delete largeArray;
}
// Cheaper
function clear() public {
largeArray = new uint256[](0);
}Conclusion
Gas optimization requires understanding EVM internals and trade-offs. Profile your contracts, measure gas costs, and apply these strategies systematically. Even small optimizations can save significant money at scale.