The Cost of Smart Contract Execution
Gas optimization is crucial for making Ethereum applications economically viable. Through careful optimization techniques, we've consistently reduced gas costs by 60% or more in production smart contracts, saving users millions in transaction fees.
At ScriptLabs Studios, we've optimized over 50 smart contracts across DeFi, NFT, and gaming projects, developing a comprehensive methodology for gas efficiency.
Understanding Gas Mechanics
Gas Cost Fundamentals
Every operation in the EVM has an associated gas cost:
- SSTORE (storage write) - 20,000 gas (first write), 5,000 gas (update)
- SLOAD (storage read) - 2,100 gas (cold), 100 gas (warm)
- CALL - 2,600 gas + execution costs
- CREATE - 32,000 gas + deployment costs
- SHA3 - 30 gas + 6 gas per word
Storage Layout Optimization
Ethereum storage slots are 32 bytes. Packing smaller variables together saves significant gas costs:
// ❌ Inefficient: Each variable uses a full storage slot
contract Inefficient {
uint128 a; // Slot 0 (wastes 16 bytes)
uint256 b; // Slot 1
uint128 c; // Slot 2 (wastes 16 bytes)
bool d; // Slot 3 (wastes 31 bytes)
}
// ✅ Optimized: Pack related variables
contract Optimized {
uint128 a; // Slot 0 (first 16 bytes)
uint128 c; // Slot 0 (last 16 bytes)
uint256 b; // Slot 1
bool d; // Slot 2 (could be packed with more variables)
}
// 🚀 Advanced packing with structs
contract Advanced {
struct PackedData {
uint64 timestamp; // 8 bytes
uint64 amount; // 8 bytes
uint64 price; // 8 bytes
address user; // 20 bytes (uses 32 bytes total)
bool active; // 1 bit, packed with address
}
mapping(uint256 => PackedData) public data;
}
Advanced Storage Patterns
Bit Manipulation for Boolean Fields
contract OptimizedFlags {
// Instead of multiple bool variables
uint256 private _flags;
uint256 private constant FLAG_PAUSED = 1;
uint256 private constant FLAG_INITIALIZED = 1 << 1;
uint256 private constant FLAG_UPGRADEABLE = 1 << 2;
uint256 private constant FLAG_TRANSFERABLE = 1 << 3;
function setPaused(bool paused) external {
if (paused) {
_flags |= FLAG_PAUSED;
} else {
_flags &= ~FLAG_PAUSED;
}
}
function isPaused() public view returns (bool) {
return _flags & FLAG_PAUSED != 0;
}
// Set multiple flags in one transaction
function setFlags(uint256 flags) external {
_flags = flags;
}
}
Packed Arrays and Mappings
contract PackedStorage {
// Pack multiple uint128 values in single slot
mapping(uint256 => uint256) private _packedPairs;
function setPair(uint256 index, uint128 value1, uint128 value2) external {
_packedPairs[index] = (uint256(value1) << 128) | value2;
}
function getPair(uint256 index) external view returns (uint128, uint128) {
uint256 packed = _packedPairs[index];
return (uint128(packed >> 128), uint128(packed));
}
// Batch operations for efficiency
function setBatch(
uint256[] calldata indices,
uint128[] calldata values1,
uint128[] calldata values2
) external {
uint256 length = indices.length;
for (uint256 i = 0; i < length; ) {
_packedPairs[indices[i]] =
(uint256(values1[i]) << 128) | values2[i];
unchecked { ++i; }
}
}
}
Function Optimization
Function Selector Optimization
Order functions by call frequency to optimize selector matching:
contract OptimizedSelectors {
// Most frequently called functions first
// Function selectors are checked in order
function transfer(address to, uint256 amount) external returns (bool) {
// Most common operation - 0xa9059cbb
return _transfer(msg.sender, to, amount);
}
function balanceOf(address account) external view returns (uint256) {
// Second most common - 0x70a08231
return _balances[account];
}
function approve(address spender, uint256 amount) external returns (bool) {
// Third most common - 0x095ea7b3
_approve(msg.sender, spender, amount);
return true;
}
// Less frequent functions later...
}
Optimized Loops and Iterations
contract LoopOptimization {
uint256[] public values;
// ❌ Inefficient loop
function inefficientSum() external view returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < values.length; i++) {
sum += values[i];
}
return sum;
}
// ✅ Optimized loop
function optimizedSum() external view returns (uint256) {
uint256 sum = 0;
uint256 length = values.length; // Cache array length
for (uint256 i = 0; i < length; ) {
sum += values[i];
unchecked { ++i; } // Skip overflow checks
}
return sum;
}
// 🚀 Assembly optimization for critical paths
function assemblySum() external view returns (uint256 sum) {
assembly {
let dataPtr := add(values.slot, 0x20)
let length := sload(values.slot)
let end := add(dataPtr, mul(length, 0x20))
for { let ptr := dataPtr } lt(ptr, end) { ptr := add(ptr, 0x20) } {
sum := add(sum, sload(ptr))
}
}
}
}
Memory and Calldata Optimization
Efficient Data Handling
contract DataOptimization {
struct User {
address wallet;
uint256 balance;
uint256 lastActivity;
bool isActive;
}
mapping(uint256 => User) public users;
// ❌ Multiple storage reads
function inefficientUserUpdate(uint256 userId, uint256 amount) external {
users[userId].balance += amount;
users[userId].lastActivity = block.timestamp;
users[userId].isActive = true;
}
// ✅ Single storage read/write
function optimizedUserUpdate(uint256 userId, uint256 amount) external {
User storage user = users[userId];
user.balance += amount;
user.lastActivity = block.timestamp;
user.isActive = true;
}
// 🚀 Memory struct for complex operations
function complexUserOperation(uint256 userId) external {
User memory user = users[userId]; // Load to memory
// Complex calculations on memory
user.balance = calculateNewBalance(user.balance);
user.lastActivity = block.timestamp;
// Single storage write
users[userId] = user;
}
}
Calldata vs Memory Parameter Optimization
contract ParameterOptimization {
// ✅ Use calldata for external functions (read-only)
function processData(uint256[] calldata data) external pure returns (uint256) {
uint256 sum = 0;
uint256 length = data.length;
for (uint256 i = 0; i < length; ) {
sum += data[i];
unchecked { ++i; }
}
return sum;
}
// ✅ Use memory when you need to modify
function modifyAndProcess(uint256[] memory data) public pure returns (uint256[] memory) {
for (uint256 i = 0; i < data.length; ) {
data[i] *= 2;
unchecked { ++i; }
}
return data;
}
// ✅ Struct parameters with calldata
struct ProcessingParams {
uint256 multiplier;
uint256 offset;
bool shouldNormalize;
}
function processWithParams(
uint256[] calldata values,
ProcessingParams calldata params
) external pure returns (uint256) {
// Process using calldata parameters
return values[0] * params.multiplier + params.offset;
}
}
Advanced Optimization Techniques
Create2 for Deterministic Deployments
contract Create2Factory {
event ContractDeployed(address indexed deployed, bytes32 indexed salt);
// Precompute addresses for gas-efficient deployment
function computeAddress(bytes32 salt, bytes32 bytecodeHash)
public view returns (address) {
return address(uint160(uint256(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
bytecodeHash
)))));
}
function deploy(bytes memory bytecode, bytes32 salt)
external returns (address deployed) {
assembly {
deployed := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
if iszero(deployed) { revert(0, 0) }
}
emit ContractDeployed(deployed, salt);
}
// Batch deployment for multiple contracts
function batchDeploy(
bytes[] memory bytecodes,
bytes32[] memory salts
) external returns (address[] memory deployed) {
uint256 length = bytecodes.length;
deployed = new address[](length);
for (uint256 i = 0; i < length; ) {
deployed[i] = this.deploy(bytecodes[i], salts[i]);
unchecked { ++i; }
}
}
}
Proxy Pattern Gas Optimization
// Optimized proxy with packed storage
contract OptimizedProxy {
// Pack implementation and admin in single slot
// implementation: 20 bytes, admin: 12 bytes = 32 bytes
bytes32 private _implementationSlot;
modifier onlyAdmin() {
require(msg.sender == getAdmin(), "Not admin");
_;
}
function getImplementation() public view returns (address) {
return address(uint160(uint256(_implementationSlot)));
}
function getAdmin() public view returns (address) {
return address(uint160(uint256(_implementationSlot >> 160)));
}
function upgrade(address newImplementation) external onlyAdmin {
_implementationSlot =
bytes32(uint256(uint160(newImplementation))) |
bytes32(uint256(uint160(getAdmin())) << 160);
}
fallback() external payable {
address impl = getImplementation();
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
}
Testing and Benchmarking
Gas Profiling with Hardhat
// hardhat.config.js
module.exports = {
gasReporter: {
enabled: true,
currency: 'USD',
gasPrice: 20, // gwei
token: 'ETH',
coinmarketcap: process.env.COINMARKETCAP_API_KEY
}
};
// Gas optimization tests
describe("Gas Optimization", function() {
it("should demonstrate storage packing savings", async function() {
const Inefficient = await ethers.getContractFactory("Inefficient");
const Optimized = await ethers.getContractFactory("Optimized");
const inefficient = await Inefficient.deploy();
const optimized = await Optimized.deploy();
// Measure deployment costs
const inefficientTx = inefficient.deployTransaction;
const optimizedTx = optimized.deployTransaction;
console.log('Inefficient deployment: ' + inefficientTx.gasUsed + ' gas');
console.log('Optimized deployment: ' + optimizedTx.gasUsed + ' gas');
console.log('Savings: ' + (inefficientTx.gasUsed - optimizedTx.gasUsed) + ' gas');
});
});
Real-World Case Studies
DeFi Protocol Optimization
For a major DeFi protocol, we achieved the following optimizations:
| Function | Original Gas | Optimized Gas | Savings |
|---|---|---|---|
| Swap | 180,000 | 95,000 | 47% |
| Add Liquidity | 220,000 | 110,000 | 50% |
| Remove Liquidity | 190,000 | 85,000 | 55% |
| Claim Rewards | 150,000 | 60,000 | 60% |
NFT Collection Optimization
contract OptimizedNFT is ERC721 {
// Pack token data efficiently
struct TokenData {
uint64 timestamp;
uint32 rarity;
uint32 level;
address creator; // 20 bytes + 12 bytes from above = 32 bytes
}
mapping(uint256 => TokenData) private _tokenData;
// Batch minting for reduced gas per token
function batchMint(address[] calldata to, uint256[] calldata amounts)
external onlyOwner {
uint256 currentTokenId = _currentIndex;
uint256 totalLength = to.length;
for (uint256 i = 0; i < totalLength; ) {
address recipient = to[i];
uint256 amount = amounts[i];
for (uint256 j = 0; j < amount; ) {
_mint(recipient, currentTokenId);
_tokenData[currentTokenId] = TokenData({
timestamp: uint64(block.timestamp),
rarity: uint32(_calculateRarity()),
level: 1,
creator: msg.sender
});
unchecked {
++currentTokenId;
++j;
}
}
unchecked { ++i; }
}
_currentIndex = currentTokenId;
}
}
Monitoring and Maintenance
Gas Usage Analytics
contract GasTracker {
mapping(bytes4 => uint256) public functionGasUsage;
mapping(bytes4 => uint256) public functionCallCount;
modifier trackGas() {
uint256 gasStart = gasleft();
_;
uint256 gasUsed = gasStart - gasleft();
bytes4 sig = msg.sig;
functionGasUsage[sig] += gasUsed;
functionCallCount[sig]++;
}
function getAverageGasUsage(bytes4 sig) external view returns (uint256) {
uint256 totalGas = functionGasUsage[sig];
uint256 callCount = functionCallCount[sig];
return callCount > 0 ? totalGas / callCount : 0;
}
function resetGasTracking(bytes4 sig) external onlyOwner {
functionGasUsage[sig] = 0;
functionCallCount[sig] = 0;
}
}
Future Optimization Trends
Layer 2 Considerations
- Arbitrum - Similar optimization principles apply
- Polygon - Lower base costs but optimization still valuable
- Optimism - Calldata costs become more significant
- StarkNet - New Cairo optimizations required
Account Abstraction Gas Optimization
// EIP-4337 compatible optimization
contract OptimizedAccountFactory {
function createAccount(address owner, uint256 salt)
external returns (OptimizedAccount account) {
bytes32 bytecodeHash = keccak256(
abi.encodePacked(type(OptimizedAccount).creationCode, abi.encode(owner))
);
address accountAddress = computeAddress(salt, bytecodeHash);
if (accountAddress.code.length == 0) {
account = new OptimizedAccount{salt: bytes32(salt)}(owner);
} else {
account = OptimizedAccount(payable(accountAddress));
}
}
}
Conclusion
Gas optimization is both an art and a science. Through systematic application of storage packing, efficient algorithms, and careful contract architecture, we consistently achieve 40-60% gas savings across different types of smart contracts.
The key principles are:
- Measure first - Always profile before optimizing
- Storage is expensive - Pack data efficiently
- Batch operations - Reduce transaction count
- Cache frequently accessed data - Avoid repeated storage reads
- Use appropriate data structures - Choose the right tool for each job
As the Ethereum ecosystem evolves with Layer 2 solutions and account abstraction, these optimization techniques remain fundamental to building cost-effective decentralized applications.
Ready to optimize your smart contracts for maximum efficiency? Let's analyze your contracts and identify optimization opportunities.
