Smart Contracts & Solidity

Chapter 12
Gas Optimization and Scalable Architecture

Advance Diploma in Blockchain
Smart Contracts & Solidity   •   Advance Diploma

Gas Optimization and Scalable Architecture

Introduction

A smart contract is both a program and a persistent public data structure. Every operation is executed by network validators, and many operations consume gas. Gas therefore links software design to economic cost, denial-of-service resistance, user experience, and scalability. Optimization is not simply the pursuit of the smallest possible bytecode. A professional Solidity engineer must balance gas efficiency with security, readability, upgradeability, auditability, and compatibility with Layer 2 systems.

This chapter examines the principal cost centres of Ethereum-compatible contracts: storage, calldata, memory, computation, loops, revert mechanisms, deployment, and cross-contract interaction. It then develops an architecture for scaling applications across Layer 1 and Layer 2 networks.

Learning Objectives

By the end of this chapter, you should be able to:

  • distinguish storage, memory, calldata, transient storage, and the stack;
  • measure execution, deployment, and data costs using appropriate tools;
  • optimize storage layout, calldata handling, loops, errors, and bytecode without weakening security;
  • explain why gas costs differ between Layer 1 and Layer 2;
  • design bounded, modular, and scalable contract architectures;
  • evaluate optimization proposals using testing, profiling, and security evidence.

1. The Gas Cost Model

Gas is an accounting unit, not a currency. A transaction specifies a gas limit and normally pays for gas used at a gas price. A simplified transaction-cost relationship is:

Transaction fee = gas used × effective gas price

Gas used is measured in gas units, while the effective gas price is measured in wei per gas unit. The resulting fee is measured in wei. On networks implementing EIP-1559, the effective gas price is generally the base fee plus the priority fee, subject to the transaction’s maximum fee and maximum priority fee. Exact costs depend on the protocol version, opcode schedule, transaction type, access list, calldata content, and refund rules.

A contract operation has several overlapping cost categories:

Cost categoryTypical sourceDesign implication
ExecutionArithmetic, hashing, branching, callsReduce unnecessary computation
State accessReading and writing storageMinimize and pack persistent state
Transaction dataCalldata and signaturesUse compact interfaces and batching
MemoryABI decoding and temporary arraysAvoid unnecessary copying
DeploymentInit code and runtime bytecodeKeep code modular and compact
External interactionCalls, logs, bridgesLimit trust and communication overhead

Gas optimization is contextual. A change that saves execution gas may increase deployment size, audit complexity, or the number of storage reads. The correct objective is not “minimum gas in every function,” but minimum economically relevant cost subject to safety and maintainability constraints.

2. EVM Data Locations and Their Costs

Storage

Storage is persistent contract state. It is organized as 32-byte slots addressed by keys derived from variable positions and, for mappings and dynamic arrays, cryptographic hashing. Storage reads and writes are expensive because they modify the replicated state of the blockchain. A first access in a transaction can be more expensive than a warm access later in the same transaction. Exact prices are protocol-defined and should be measured against the target network’s current specification.

A storage write should occur only when state must survive the transaction. For example, a local accumulator should remain in memory or on the stack until its final value is known:

function sum(uint256[] calldata values) external pure returns (uint256 total) {
    for (uint256 i; i < values.length; ++i) {
        total += values[i];
    }
}

The function avoids writing an intermediate result to storage. It is also pure, communicating that no blockchain state is read or changed.

Memory

Memory is temporary and expanded in 32-byte words. It is cheaper than storage but is not free: expansion has a linear component and a quadratic component in the size of memory used. Consequently, allocating very large temporary arrays can become costly. Memory is cleared after execution.

Calldata

Calldata is read-only transaction input. External functions should generally accept arrays and strings as calldata when they do not need modification. This avoids copying the complete argument into memory. Calldata is especially important on Layer 2 because transaction data may form a substantial part of the fee paid to the settlement layer.

Stack and transient storage

The EVM stack holds short-lived values and is efficient, but it has a limited depth and can become difficult to manage in complex expressions. Transient storage, where supported by the target EVM, provides transaction-scoped key-value storage: values disappear at the end of the transaction. It can be useful for reentrancy locks and temporary coordination, but contracts must not assume availability on every network.

3. Storage Layout and Data Design

Packing value types

Solidity may pack smaller value types into one 32-byte storage slot when declaration order and type compatibility permit it. For example, several flags or bounded counters may occupy one slot. Packing can reduce the number of storage slots, but it is not automatically beneficial. Updating one packed field may require reading, masking, and rewriting the entire slot. If fields are frequently updated independently, separate slots may sometimes be clearer and competitive in cost.

Packing is appropriate only when the selected range is safe. Replacing uint256 with uint64 saves storage only if the variables can share a slot; it also introduces a range limit. A narrower type must never be used merely as a cosmetic optimization.

Mappings and arrays

Mappings provide efficient lookup by key but do not provide enumeration. Dynamic arrays provide ordering and length but can make deletion and iteration expensive. A common scalable pattern is to store membership in a mapping and maintain a separate index or off-chain event stream when enumeration is needed.

Deleting an array element by shifting all later elements has cost proportional to the number of shifted elements. “Swap and pop” removes an element in approximately constant storage operations, but changes ordering:

function remove(uint256 index) internal {
    uint256 last = items.length - 1;
    if (index != last) items[index] = items[last];
    items.pop();
}

The caller must validate index, and any index mapping must also be updated. This technique is unsuitable when stable ordering is part of the contract’s specification.

Caching storage reads

Repeatedly reading the same storage variable wastes gas and obscures intent. A local variable can cache a value, after which one final write can commit the result. However, caching a value that another external call can change creates stale-state risk. Checks-effects-interactions ordering and reentrancy analysis remain more important than a small saving.

4. Calldata, ABI Design, and Events

External functions should use the narrowest semantically correct interface. calldata parameters are read-only and avoid unnecessary memory copying. ABI encoding has overhead, so batching can reduce per-transaction fixed overhead, but large batches increase execution cost and may approach the block or per-transaction gas limit.

A batch function should therefore have an explicit maximum size or a design that supports pagination. A loop over user-supplied data must not assume that an array is small. The contract should either reject oversized batches or make partial progress safe and observable.

Events are cheaper than persistent storage for information intended primarily for off-chain indexing, although logs still consume gas and are not directly readable by contracts. Events should not replace state required for future on-chain authorization or accounting. Indexing topics improves queryability but adds log cost and is limited by the event ABI.

5. Loops and Denial-of-Service Resistance

The gas of a bounded loop can be modelled approximately as:

Gtotal ≈ Gfixed + n × Giteration

where n is the number of iterations, Gfixed is the non-repeating cost, and Giteration is the average cost per iteration. This is an engineering estimate, not a protocol pricing rule; iteration cost may vary with branches, storage warmness, external calls, and data values.

Unbounded loops over storage are a classic denial-of-service risk. If an attacker can increase the collection indefinitely, a maintenance function may eventually exceed the block gas limit and become unusable. Safer alternatives include:

  • bounded batches with a cursor;
  • pull-based claims, where each user processes their own entitlement;
  • checkpointing and resumable state transitions;
  • off-chain indexing with Merkle-proof verification;
  • data structures with predictable deletion and lookup costs.

The unchecked block can remove overflow checks in carefully proven regions, such as a loop counter that is known to remain below the array length. It must not be used for balances, prices, lengths, or arithmetic exposed to hostile inputs without an explicit invariant and tests.

6. Errors, Reverts, and Control Flow

Custom errors are generally more compact than revert strings because the selector and encoded arguments replace a long textual message. They also provide structured information to clients:

error Unauthorized(address account);

modifier onlyOwner() {
    if (msg.sender != owner) revert Unauthorized(msg.sender);
    _;
}

Errors should reveal enough information for debugging without disclosing sensitive assumptions or creating misleading interfaces. A revert is not a substitute for input validation. Checks should be performed before state changes and external calls.

The require statement remains readable and appropriate for many conditions. The choice between a custom error and a string should consider compiler support, tooling, user-facing diagnostics, and the project’s coding standard. Optimization that makes a failure mode incomprehensible is poor engineering.

7. Compiler and Bytecode Optimization

The Solidity optimizer can reduce runtime gas or deployment size by transforming code. Optimizer settings represent a trade-off between deployment cost and expected execution frequency. A contract deployed once but called millions of times may justify settings that increase deployment size to reduce runtime cost. A short-lived contract factory may favour smaller deployment cost.

The compiler version, optimizer configuration, EVM target, libraries, metadata, and proxy architecture must be fixed and documented. Production bytecode must be verified. Developers should compare benchmark results before and after each change rather than relying on intuition.

External libraries can reduce duplicated bytecode when linked appropriately, but calls may introduce overhead and trust boundaries. Internal functions may be inlined, increasing bytecode size. Contract size limits also matter: excessive monolithic code can become undeployable or difficult to audit.

Proxy systems reduce the need to redeploy state but add delegatecall, storage-layout, upgrade-authority, and initialization risks. Upgradeability is an architectural governance decision, not merely a gas technique.

8. Measurement and Optimization Workflow

A rigorous workflow is more reliable than isolated micro-optimizations.

  1. Define the workload. Identify common, rare, administrative, deployment, and failure paths.
  2. Establish a baseline. Record gas estimates and measured gas for representative transactions.
  3. Profile. Use compiler reports, local tracing, fork-based tests, and network-specific measurements.
  4. Form a hypothesis. For example, “one storage write can be replaced by an event.”
  5. Change one relevant variable. Preserve behaviour and access control.
  6. Test invariants and adversarial cases. Include fuzzing, property tests, and reentrancy scenarios.
  7. Measure again. Compare median and worst-case costs, not only a favourable example.
  8. Review maintainability. Document why the optimization is safe and necessary.

Useful metrics include gas per successful call, gas per reverted call, deployment gas, bytecode size, maximum batch size, and cost per business operation. A test that records only one gas number may conceal costs caused by cold storage access or large calldata.

9. Layer 2 and Scalable Architecture

Layer 2 systems execute transactions away from Ethereum mainnet while using Ethereum for settlement, security, data availability, or dispute resolution. Their fee models differ. On some rollups, execution is relatively inexpensive while posting transaction data to Layer 1 is a significant component. On other systems, proving, sequencing, or data-availability charges have different effects. Therefore, a Layer 2 optimization should be based on the target chain’s published fee model rather than assumptions about Layer 1 gas.

A useful conceptual decomposition is:

Ftotal = Fexecution + Fdata + Fsettlement + Fservice

where Ftotal is the user’s total fee, and the components represent execution, data publication, settlement, and any chain-specific service charge. The formula is a conceptual model, not a universal billing formula; a particular network may combine or rename components.

Architectural principles

Separate hot and cold paths. Frequently used user operations should be small and predictable. Rare administration, migration, and analytics should not burden every user call.

Minimize on-chain data. Store values needed for future verification; emit or publish other data through appropriate, authenticated channels. Merkle roots, signed authorizations, and proof-based claims can replace large on-chain lists, but they introduce proof-generation, key-management, and availability responsibilities.

Use asynchronous boundaries. Bridges, messaging systems, and cross-domain calls may be delayed or fail. State machines should represent pending, completed, and failed states rather than assuming synchronous finality.

Design for replay and idempotence. Cross-chain messages should include a unique identifier, source domain, destination, nonce or sequence, and an executed marker. Replaying a delivered message must not repeat its effect.

Avoid synchronous composability assumptions. A Layer 2 application should not assume that every asset, oracle, or contract exists at the same address or has identical finality on every network.

10. Illustrative Case: Scalable Rewards Distribution

Background

Consider an illustrative rewards protocol that distributes tokens to 100,000 users each month. The initial design stores every user’s reward in a mapping and has an administrator loop through all users, transferring tokens in one transaction.

Problem

The loop grows with the number of users. It may exceed the block gas limit, requires the administrator to pay a large fee, and creates a single operational failure point. Repeating storage writes also makes the design expensive on both Layer 1 and Layer 2.

Analysis

The original approach has approximate work proportional to n, the number of users. It also creates a poor failure model: one invalid recipient or unexpected token behaviour can revert the entire distribution. The protocol needs verifiable accounting, bounded execution, and a user-accessible recovery path.

Approach

The redesigned system computes a Merkle root of off-chain allocations and stores only the root and distribution epoch on-chain. A user submits a proof containing their allocation and claims once. The contract verifies the proof, checks a claimed bitmap or mapping, updates the claim state, and transfers the tokens. Claims are independent, so one user’s failure does not block others.

For large-scale operational requirements, the protocol may use a pull-based escrow, a bounded batch claim function, or a Layer 2 deployment. The root update must be authorized, epoch transitions must be protected against replay, and token transfer behaviour must be checked using a well-tested safe-transfer library.

Outcome

Persistent state changes are reduced from one per user in the administrative transaction to one per claimant. Work is distributed across transactions, and users can claim when convenient. The trade-off is that users must obtain proofs, the root-generation process must be auditable, and incorrect allocation data cannot be repaired casually without a defined correction mechanism.

Learning points

The case illustrates that scalable architecture often produces larger savings than instruction-level optimization. It also shows that gas reduction changes governance and operational risk. Off-chain computation is acceptable only when the on-chain verification rule, data availability, authorization, and correction process are explicit.

11. Security, Governance, and Ethical Considerations

Gas optimization must never weaken authorization, accounting precision, pause controls, or emergency recovery. Packed variables can create truncation errors; unchecked arithmetic can permit insolvency; assembly can bypass compiler safety assumptions; and proxy upgrades can change economic rules after users deposit assets.

Governance documents should identify who controls upgrades, roots, fee parameters, pausing, and supported domains. Users should be informed when a low-cost design moves computation or data responsibility off-chain. A system that is cheap for the operator but opaque or inaccessible to users may be economically efficient yet ethically deficient.

Auditors should review gas-sensitive code for semantic changes, not only benchmark improvements. AI-assisted coding and optimization tools may suggest useful transformations, but generated code requires compilation, differential testing, fuzzing, formal reasoning where appropriate, and human security review. AI must not be treated as evidence of correctness.

12. Best-Practice Checklist

  • Measure before optimizing and record the target network and compiler configuration.
  • Prefer clear storage models over clever but undocumented packing.
  • Use calldata for read-only external inputs where appropriate.
  • Bound every user-influenced loop or make it resumable.
  • Cache storage reads only when doing so preserves correctness.
  • Use custom errors when structured, compact reverts are appropriate.
  • Separate persistent state from event-only information.
  • Test deployment cost, runtime cost, revert paths, and maximum batch sizes.
  • Treat Layer 2 data publication and cross-domain messaging as architectural concerns.
  • Document invariants for unchecked, assembly, transient storage, and upgrade logic.
  • Prefer pull-based and idempotent workflows for large populations.
  • Re-audit security after any gas-driven refactor.

Chapter Summary

Gas is a protocol-defined accounting mechanism linking computation, state, data, and economic cost. Storage is persistent and generally expensive; memory and calldata are temporary alternatives with different copying and expansion behaviour. Efficient contracts reduce unnecessary storage writes, use suitable data locations, pack values only when safe, and avoid unbounded loops.

Custom errors, compiler optimization, bytecode management, and carefully selected batching can reduce cost, but optimization must be measured against security and maintainability. Layer 2 systems change the cost model rather than eliminating it: data publication, settlement, execution, and messaging must be analysed together. Scalable designs use bounded work, pull-based claims, proofs, asynchronous state machines, replay protection, and explicit governance. The professional standard is evidence-based optimization that preserves correctness and makes its assumptions auditable.

Key Terms

Gas: The EVM unit used to account for computational and state-transition work.

Calldata: Read-only transaction input supplied to an external function.

Storage: Persistent contract state maintained by the blockchain.

Memory: Temporary, transaction-scoped data space used during execution.

Transient storage: Transaction-scoped key-value storage supported by relevant EVM versions.

Storage packing: Placing compatible value types in shared storage slots.

Cold access: The first access to a relevant account or storage location in a transaction.

Warm access: A later access whose cost is reduced under the applicable gas rules.

Custom error: A typed Solidity revert mechanism with compact encoded data.

Bytecode: EVM instructions produced by compilation.

Optimizer: A compiler component that transforms code to reduce selected costs.

Batching: Processing multiple logical operations in one transaction.

Bounded loop: A loop whose maximum work is constrained by design.

Pull payment: A model in which each beneficiary claims their own entitlement.

Merkle proof: A cryptographic proof that a value belongs to a committed tree root.

Layer 2: A scaling system that executes transactions outside the main settlement layer.

Idempotence: The property that repeating an operation does not repeat its effect.

Delegatecall: A call executing another contract’s code in the caller’s storage context.

Gas griefing: Deliberately causing excessive or unusable gas consumption for another party.

Review Questions

Short Answer Questions

  1. Why is gas described as an accounting unit rather than a currency?
  2. What is the principal difference between storage and calldata?
  3. Why can an unbounded loop create a denial-of-service risk?
  4. What problem do custom errors address?
  5. Name two reasons why Layer 2 fee analysis differs from Layer 1 analysis.

Long Answer Questions

  1. Explain how storage layout, caching, and data-location choices affect gas and correctness.
  2. Compare dynamic arrays, mappings, and swap-and-pop deletion for different application requirements.
  3. Describe a rigorous workflow for measuring and validating a gas optimization.
  4. Explain how pull-based claims and Merkle roots can improve scalability, including their trade-offs.
  5. Discuss why upgradeability and gas optimization must be treated as governance and security decisions.

Analytical / Application Questions

  1. A contract loops over a user-controlled list of recipients and transfers tokens to each. Design a safer scalable alternative and identify its security assumptions.
  2. A proposed optimization replaces several uint256 fields with narrower types and adds unchecked arithmetic. Evaluate the proposal, specifying the tests and invariants required before acceptance.
  3. Compare deploying a rewards contract on Layer 1 with deploying it on a rollup. Construct a fee and operational analysis using execution cost, data cost, settlement, finality, and cross-chain risks.

Multiple Choice Questions with Answers

Question 1

Which data location is generally most appropriate for a read-only dynamic array passed to an external Solidity function?

A. storage

B. memory

C. calldata

D. transient storage

Correct Answer: C. calldata

Question 2

What is the principal scalability risk of an unbounded loop over a user-influenced collection?

A. The contract becomes unable to emit events

B. The operation may exceed the available gas limit

C. The compiler automatically deletes the collection

D. Every iteration becomes free

Correct Answer: B. The operation may exceed the available gas limit

Question 3

Which technique removes an array element with approximately constant work but does not preserve ordering?

A. Shifting every later element left

B. Swap and pop

C. Sorting the array first

D. Copying the array to calldata

Correct Answer: B. Swap and pop

Question 4

What is a principal advantage of Solidity custom errors over long revert strings?

A. They bypass all authorization checks

B. They are structured and often more compact

C. They prevent every transaction from reverting

D. They permanently store diagnostic text

Correct Answer: B. They are structured and often more compact

Question 5

In the approximation Gtotal ≈ Gfixed + n × Giteration, what does n represent?

A. The gas price in wei

B. The number of iterations

C. The number of storage slots in the EVM

D. The contract nonce

Correct Answer: B. The number of iterations

Question 6

Why can packing several fields into one storage slot sometimes increase complexity?

A. Packed fields cannot be read by Solidity

B. Updating one field may require masking and rewriting the slot

C. Packing always makes values immutable

D. Packed fields are stored in calldata

Correct Answer: B. Updating one field may require masking and rewriting the slot

Question 7

Which architecture best prevents one failed beneficiary claim from blocking all other beneficiaries?

A. One administrator loop over every beneficiary

B. A pull-based claim process

C. A single transaction with no validation

D. A permanently locked escrow

Correct Answer: B. A pull-based claim process

Question 8

Which statement about Layer 2 gas optimization is most accurate?

A. Layer 2 removes all transaction costs

B. Only EVM execution matters on every Layer 2

C. Data publication and settlement can be important fee components

D. Layer 2 contracts never need replay protection

Correct Answer: C. Data publication and settlement can be important fee components

Question 9

What property ensures that replaying a delivered cross-domain message does not repeat its effect?

A. Idempotence

B. Randomness

C. Recursion

D. Opacity

Correct Answer: A. Idempotence

Question 10

What should normally occur before accepting a gas optimization in production?

A. Delete all tests to reduce repository size

B. Measure it, test security properties, and review maintainability

C. Assume the optimizer guarantees correctness

D. Replace all storage with unchecked assembly

Correct Answer: B. Measure it, test security properties, and review maintainability

Smart Contracts & Solidity
Chapter 12 of 15