RoadToChain Logo
RoadToChain
T1/M1.4/Optimizing Smart Contract Storage
beginner 14m read

Optimizing Smart Contract Storage

Refactoring state layout, slot packing, O(1) mappings, and lightweight event index architectures.

#gas #packing #optimization

Let's address the final architectural phase of our Evolutionary Voting System:

We started with a naive contract that stored strings, looped over voter arrays, and triggered out-of-gas errors. We discovered that a PostgreSQL mindset leads to a frozen ledger. We realized that blockchain is not a database, but a trust primitive.

Now, we are ready to write the production-grade version of our contract.

I genuinely felt a huge sense of relief when I completed this final refactoring pass early on. By discarding the loose variables, packaging storage slots, and shifting query indices off-chain, we dropped transaction gas consumption by over 85%!

To write production-ready Solidity, you must learn to model your contract like a precision Swiss clock.


1. The Metaphor: The Precision Swiss Clockwork

Imagine the internal mechanics of a high-end luxury clock:

  • The Naive System (The Plastic Gearbox): You build a clock using thick, loose plastic gears. It takes up a massive amount of case space, requires a heavy spring to turn, and grinds to a halt if you try to make it run faster.
  • The Optimized System (The Jewel Movement): A Swiss watchmaker carves tiny, precise gold gears, fits them together with zero gaps, and mounts them on ruby pivot bearings. The mechanism occupies a fraction of the space, turns with a microscopic touch of energy, and runs flawlessly for decades.

Optimizing your storage slot layout, replacing dynamic variables with 32-byte hashes, and grouping types sequentially is the watchmaking of Solidity engineering.


// Reality Check

Every single state variable write (SSTORE) in Solidity costs between 5,000 and 20,000 gas. If your contract reads or writes variables inefficiently inside a loop, it will bleed user capital on every transaction. In production, gas optimization is not just a game; it is a mandatory security discipline to prevent block exhaust attacks.

— Production Engineering Principle
Storage Slot Packing — Optimizing Gas Costs
Packing struct variables sequentially to fit within single 32-byte slots reduces on-chain storage SSTORE operations, saving up to 50% in transaction gas fees.

2. The Optimized Code: The Production Standard

Here is the final, fully optimized, production-ready version of our Evolutionary Voting contract:

SmartAccount.sol
solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
 
// ✅ THE PRODUCTION-GRADE IMPLEMENTATION
contract OptimizedVoting {
    // 1. Pack Struct sequential variables to squeeze into 1 storage slot!
    struct Candidate {
        uint248 voteCount; // 31 bytes (Slot 0 - Packed!)
        bool isActive;      // 1 byte  (Slot 0 - Packed!)
    }
 
    // 2. Remove strings! Reference candidates via 32-byte unique hash keys
    mapping(bytes32 => Candidate) public candidates;
    
    // 3. Constant-time O(1) voting validation mapping
    mapping(address => bool) public hasVoted;
    
    // 4. Lightweight counter eliminates loops entirely!
    uint256 public totalVotersCount;
 
    error AlreadyVoted();
    error CandidateNotActive();
 
    // Broadcasts historical feeds cheap to off-chain subgraphs
    event VoteCast(address indexed voter, bytes32 indexed candidateId);
    event CandidateRegistered(bytes32 indexed candidateId);
 
    function registerCandidate(bytes32 _candidateId) public {
        candidates[_candidateId] = Candidate({
            voteCount: 0,
            isActive: true
        });
        emit CandidateRegistered(_candidateId);
    }
 
    function vote(bytes32 _candidateId) public {
        if (hasVoted[msg.sender]) revert AlreadyVoted();
        if (!candidates[_candidateId].isActive) revert CandidateNotActive();
 
        hasVoted[msg.sender] = true;
        candidates[_candidateId].voteCount += 1; // Constant time write!
        totalVotersCount += 1; // Constant time increment!
 
        emit VoteCast(msg.sender, _candidateId);
    }
}

Why is this precision clockwork?

  1. Packed Struct: uint248 takes 31 bytes, and bool takes 1 byte. The compiler packs them sequentially into a single 32-byte storage slot (Slot 0). Initializing a candidate takes exactly one SSTORE call instead of two!
  2. bytes32 _candidateId: We replaced dynamic string names with 32-byte cryptographic identifier hashes. The strings are stored off-chain in a Web2 database or in a Subgraph registry mapping candidate hashes to names.
  3. No loops: We deleted address[] voterList and the unbounded loop. We check authorization using the O(1) hasVoted mapping and track total participants using the simple totalVotersCount counter.
  4. Megaphone Events: We emit VoteCast events so off-chain indexes can sync and display real-time chronological feeds to our website dashboard instantly.

System Design Challenge
Think Active

Analyze the gas cost difference between NaiveVoting and OptimizedVoting when 1,000 votes have been cast. Why does the gas cost of calling vote in the optimized version remain exactly the same for the 1st voter and the 1,000th voter, while the naive version's cost increases with every vote?

[ Think Before Continuing ]

Was this lesson helpful?

Let us know what you think of this specification. (submitting anonymously)