Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 5

Understanding Data Locations: Storage, Memory, and Calldata

Recommended resources

Hello! Welcome to the fifth lesson in our "Solidity Foundations" module.

In our last session, we explored control flow constructs like if, for, and while. We saw that these are more than just syntactic conveniences; they compile down to JUMP and JUMPI opcodes and have profound implications for gas consumption and contract security, especially when dealing with loops over user-controlled data.

Introduction

Today, we're taking the deep dive I promised into the EVM's data architecture. The learning outcome for this lesson is to explain and correctly use storage, memory, and calldata data locations.

Every variable in your contract lives in one of these three areas. Understanding their distinct characteristics is not just an academic exercise; it's fundamental to writing gas-efficient, secure, and correct smart contracts. Given your interest in EVM internals and optimization, this lesson is designed to give you a precise mental model of where your data is, how it's laid out, and how much it costs to access it.

We'll dissect each location:

  • storage: The contract's persistent "hard drive."
  • memory: The temporary "RAM" for function execution.
  • calldata: The read-only "input buffer" for external function calls.

Let's begin by establishing a high-level overview of these three crucial concepts.


The Three Data Locations: An Overview

At a glance, these three memory types serve very different purposes and have vastly different cost profiles.

What is Smart Contract Storage Layout?

This article from Alchemy Docs provides a clear, high-level introduction to the three Solidity memory types. It's a great starting point to frame our discussion.

Read the section titled 'Solidity Memory Types', including the subsections for 'Memory', 'Calldata', and 'Storage'. Focus on their purpose and lifecycle (i.e., when are they created and destroyed).

To summarize the key distinctions:

Data Location Persistence Mutability Cost Typical Use
storage Persistent (across transactions) Read/Write Very High State variables
memory Volatile (per function call) Read/Write Medium Temporary variables in functions
calldata Volatile (per function call) Read-only Very Low External function arguments

Now, let's explore each of these in detail, starting with the most critical and expensive: storage.


The Heart of the Contract: storage

Contract storage is the persistent, on-chain key-value store that holds your contract's state. You can think of it as a vast, virtual array of 2²⁵⁶ slots, where each slot is 32 bytes (256 bits). All state variables (those declared at the contract level) reside here.

The way Solidity organizes these variables into slots is a core concept for both gas optimization and understanding advanced contract patterns.

Mastering Solidity Storage: Essential Mapping Secrets You Need to Know!

This video by Jesper Kristensen provides an excellent visual explanation of the storage layout. It masterfully demonstrates how simple variables, packed variables, and mappings are placed within these storage slots.

Please watch the following two segments: Simple Types and Packing (from 01:51 to 10:55): This explains how basic variables are laid out sequentially and how Solidity packs smaller variables into a single slot to save space and gas. Mappings (from 10:55 to 22:01): This reveals the clever hashing mechanism used to place mapping values in storage, preventing collisions with other state variables.

Let's break down the key rules demonstrated in the video:

  1. Sequential Layout: Statically-sized state variables are laid out in storage in the order they are declared, starting from slot 0.

  2. Variable Packing: If multiple, consecutive variables are smaller than 32 bytes, the compiler will try to "pack" them into a single storage slot. For example, two uint128 variables or a uint128, a uint64, and another uint64 can all fit into one 32-byte slot. This is a crucial gas optimization technique.

  3. Mappings and Dynamic Arrays: These types are treated differently. Because their size isn't known at compile time, their declared slot (e.g., slot p) is left empty and acts as a placeholder. The actual data is stored at a different location derived from a keccak256 hash.

    • For a mapping myMap at slot p, the value for myMap[key] is stored at slot keccak256(h(key) . p), where . is concatenation and h(key) is the 32-byte representation of the key.
    • This hashing strategy scatters the data across the vast storage space, ensuring a dynamically growing mapping won't overwrite the next state variable.

This "rulebook" for storage layout is essential. The NetSPI article "EVM Internals - Part 2" (resource ID LINK) provides a comprehensive textual reference for the storage layout of every data type, including strings, enums, and multi-dimensional arrays, which you can use for future reference.


The Cost of Persistence: SSTORE and SLOAD

Why is understanding storage layout so important? Because storage operations are among the most expensive in the EVM. The opcodes for writing and reading from storage, SSTORE and SLOAD, are costly because they involve modifying the blockchain's state, which every node must process and agree upon.

The cost is not even static. EIP-2929 (introduced in the Berlin hard fork) changed the gas metering to distinguish between "warm" and "cold" storage access. Accessing a storage slot for the first time in a transaction ("cold" access) is significantly more expensive than accessing it again ("warm" access).

Ethereum Gas Optimisation: Effects of recent EIPs, architecture, and design and impl choices

This advanced talk from the Ethereum Engineering Group digs into the nuances of gas optimization, including the complex gas cost algorithm for SSTORE.

Watch from 05:04 to 12:20. The speaker walks through the incredibly complex gas calculation for an SSTORE operation, considering the original, current, and new values. Don't worry about memorizing the exact algorithm; the goal is to appreciate the complexity and understand the key drivers of cost, such as changing a slot from zero to non-zero.

The key takeaway is that writing to storage, especially for the first time or changing a value from zero to non-zero, is very expensive. This reinforces the importance of variable packing and minimizing state changes whenever possible.


The Scratchpad: memory

If storage is the hard drive, memory is the RAM. It's a temporary, volatile data area that is created for each function call and destroyed when the call ends. It's cheaper than storage but more expensive than calldata.

You use memory to hold and manipulate complex data types (like structs or arrays) within a function without incurring the high cost of SSTORE for every modification.

The cost of memory is not per-write, but rather based on how much you use. It's calculated based on the highest memory address accessed, and it grows quadratically.

Ethereum Gas Optimisation: Effects of recent EIPs, architecture, and design and impl choices

The same gas optimization talk also provides the formula for memory expansion cost.

Watch the short segment from 39:04 to 41:37. This explains the algorithm for memory expansion cost. The key insight is that the cost scales with the square of the memory size, so allocating very large temporary arrays can become surprisingly expensive.


The Input Tray: calldata

calldata is a special, read-only data location where the arguments for external function calls are stored. It is the cheapest data location because it doesn't require any copying from the transaction payload into memory—the EVM can read directly from it.

Rule of thumb: For any external function that takes reference types (like arrays, strings, or structs) as arguments, you should always declare them as calldata unless you absolutely need to modify them within the function.

// GOOD: `data` is read-only and cheap.
function processData(uint[] calldata data) external pure {
    // ... read from data ...
}

// BAD: `data` is copied to memory, which is more expensive.
function processData(uint[] memory data) external pure {
    // ... read from data ...
}

The video from the Ethereum Engineering Group briefly touches upon this, noting that you can save gas by explicitly using calldata to prevent the compiler from automatically copying parameters to memory (57:07 to 57:55).


Putting It All Together: The Keywords in Action

Now that we understand the concepts, let's see how we use the storage, memory, and calldata keywords in our code. These keywords are required for reference types (arrays, structs, strings, mappings) when they are used as function parameters or as local variables inside functions.

Ethereum Virtual Machine Internals – Part 2

The NetSPI article provides a clear definition of when and how to use these keywords.

Read the section titled 'Data Location Keywords and Reference Types'. This section succinctly explains how these keywords act as pointers or create copies, and the rules for their usage.

Here is a practical example demonstrating the differences:

pragma solidity ^0.8.18;

contract DataLocationsExample {
    
    // This array lives in STORAGE.
    uint[] public myStorageArray;

    // `newNumbers` is in CALLDATA. It's the cheapest way to pass in external data.
    // It's read-only.
    function sumCalldata(uint[] calldata newNumbers) external pure returns (uint) {
        uint total = 0;
        // We can read from calldata in a loop.
        for(uint i = 0; i < newNumbers.length; i++) {
            total += newNumbers[i];
        }
        return total;
        // newNumbers[0] = 1; // This would cause a compile error! Calldata is read-only.
    }

    // `myStorageArray` is copied into a new array in MEMORY.
    function getReversed() external view returns (uint[] memory) {
        // Create a new array in memory.
        uint[] memory reversedArray = new uint[](myStorageArray.length);
        
        uint len = myStorageArray.length;
        for(uint i = 0; i < len; i++) {
            // We can write to the memory array.
            reversedArray[len - 1 - i] = myStorageArray[i];
        }
        return reversedArray;
    }

    // `storagePointer` is a pointer to `myStorageArray` in STORAGE.
    // Assignments to it modify the original state variable.
    function doubleInPlace() external {
        // 'a' is a local variable that points to the state variable. No copy is made.
        uint[] storage a = myStorageArray;
        
        for(uint i = 0; i < a.length; i++) {
            // This modifies `myStorageArray` directly, incurring SSTORE costs.
            a[i] = a[i] * 2;
        }
    }
}

Take a moment to read through the comments in this example. It directly contrasts the use cases and behaviors of all three data locations.


Conclusion

You've now dissected one of the most fundamental aspects of the EVM. A solid understanding of data locations is what separates a novice from an expert Solidity developer. It directly impacts the gas efficiency, security, and correctness of your contracts.

Key Takeaways:

  • storage is persistent, expensive, and structured in 32-byte slots. Its layout follows strict rules of declaration order and variable packing.
  • memory is a temporary, cheaper scratchpad for data manipulation within a function call. Its cost grows quadratically with usage.
  • calldata is a read-only, temporary location for external function arguments, offering the lowest gas cost for passing data into a contract.
  • Choosing the correct data location is a primary lever for gas optimization. Always prefer calldata for external function arguments if they don't need to be modified. Use memory to avoid repeated SLOADs and SSTOREs within a function.

In our next lesson, we will build directly on this knowledge as we learn to define and use structs and enums to model domain data. Now that you know exactly how they are laid out in storage, you'll be able to design them in a way that is both logical and gas-efficient from the start.