ScriptLabs

ServicesTechnologyPortfolioAboutInsightsContact
Sign InSign Up
HomeInsightsBlogBlockchain & Web3
Blockchain & Web3

Solidity Gas Optimization: Reducing Transaction Costs by 60%

Advanced techniques for Ethereum smart contract efficiency

Fernando A McKenzie
Fernando A McKenzie
Founder
January 5, 2024
10 min read

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.