RoadToChain Logo
RoadToChain
T1/M1.1/Storage vs Memory
beginner 12m read

Storage vs Memory

Storage, memory, and calldata. Why copying arrays behaves differently and costs different gas.

#storage #memory #calldata

Let's look at a weird Solidity bug that leaves almost every beginner pulling their hair out:

You write a function to update an item in a list of users. You fetch the user, modify their status field to true, and exit. But when you check the blockchain state later, absolutely nothing changed. The user's status is still set to false.

I genuinely made this mistake early on. I spent hours debugging my frontend, thinking MetaMask was sending junk arguments, before I realized the compiler was silently warning me about a fundamental concept: Data Locations.


1. The Story: The Phantom Username Editor

In my first Solidity project, I built a user registry. Users could register and later edit their display usernames.

SmartAccount.sol
solidity
function updateUsername(uint256 _index, string memory _newName) public {
    User memory tempUser = users[_index];
    tempUser.username = _newName; 
    // Transaction succeeds, gas is burned... but username never updates on-chain!
}

When I tested it, I registered as "Alice", clicked edit, typed "Bob", and submitted the transaction. It confirmed. But when my React frontend reloaded, it still read "Alice". I checked Etherscan: the transaction went through successfully. I looked at my code, re-read the Solidity docs, and felt completely lost. Why was my state refusing to write?


2. The Metaphor: The Blueprint and the Stone Wall

To make this instantly clear, let's look at a construction analogy (tightly restricted to the essentials):

  • storage (The Stone Wall): Working directly on the physical stone wall of your building. If you pick up a chisel and carve a window into the stone wall, that window exists permanently.
  • memory (The Paper Blueprint): Photocopying the blueprint of the stone wall onto a sheet of paper. Drawing a window on the paper blueprint with a pencil does not change the physical stone wall.
  • calldata (The Sealed Cargo List): A sealed delivery manifest received from a cargo truck. You can read the list of items through the glass, but you cannot edit or alter the paper. It is strictly read-only.

3. The Visual State Routing Diagram

Our frontend can intercept and render the exact state data flow. Notice how declaring a variable as memory breaks the connection to permanent storage, routing changes to a temporary sandbox instead:

SmartAccount.sol
                  +-----------------------------------------------+
                  |              EVM STATE STORAGE                |
                  |             (Node SSD - Stone Wall)           |
                  |  users[0] = { name: "Alice", isActive: true }  |
                  +-----------------------------------------------+
                          │                               │
              (storage pointer)                     (memory copy)
                          │                               │
                          ▼                               ▼
              User storage pointerUser;            User memory tempUser;
            [ Points to persistent slot ]       [ Copies struct to RAM ]
                          │                               │
              Updates users[0] directly          Updates tempUser in RAM
                          │                               │
                          ▼                               ▼
              [ Written to Blockchain! ]           [ Shredded on Return! ]

[!TIP] VISUAL TRIGGER FOR FRONTEND: This diagram represents how data mutations are routed inside the EVM. When animating this flow in the UI, highlight the active green path representing storage writing directly back to the persistent storage box, vs the red path showing memory data loading into a floating box and dissolving when the execution terminates.

Solidity Storage vs Memory vs Calldata
Storage writes persist permanently on-chain (expensive), memory is a cheap temporary copy in RAM, and calldata is a read-only transaction payload reference.

4. Technical Explanation: EVM Data Allocations & Slot Mechanics

Now, let's look at what is happening inside the Ethereum Virtual Machine (EVM) under the hood.

Solidity manages variables using three distinct memory spaces, each behaving differently and costing vastly different amounts of gas:

1. State Storage (storage)

  • Under the Hood: A contract's storage is a persistent key-value store mapping 32-byte keys to 32-byte values. It is written permanently to the blockchain ledger and stored on the hard drives of thousands of nodes globally.
  • Gas Mechanics: Writing to storage is the most expensive operation in the EVM. An SSTORE opcode costs 20,000 gas when writing to an empty slot, and 5,000 gas when modifying an existing slot.
  • How Pointer References Work: When you write User storage pointerUser = users[_index], the compiler does not copy any state variables. Instead, it creates an internal storage pointer that contains the raw slot coordinate of that user in contract storage. When you modify pointerUser.isActive, the EVM executes a direct SSTORE on that exact slot coordinate.

2. Temporary Memory (memory)

  • Under the Hood: Memory is a temporary byte-array allocated dynamically during transaction execution. It is initialized to zero on every function call and is completely wiped clean the millisecond the function returns.
  • Gas Mechanics: Memory is extremely cheap. Reading or writing a 32-byte slot in memory costs only 3 gas.
  • How Copy Assignments Work: When you write User memory tempUser = users[_index], the EVM loads all variables of the struct from the node's hard drive (SLOAD) and copies them into sequential memory offsets in RAM. Modifying tempUser.isActive writes to the memory offset (3 gas) but never triggers an SSTORE to the blockchain. When the execution exits, this memory is cleared, and your changes are lost forever.

3. Read-Only Arguments (calldata)

  • Under the Hood: Calldata is a temporary, non-modifiable byte-array where the incoming transaction parameters (the data payload) are stored.
  • Gas Mechanics: Calldata cannot be modified at all. Because it points directly to the raw bytes sent in the transaction data field, the EVM does not need to allocate new memory slots or perform any copying, making it the absolute cheapest way to read incoming function arguments.

// Reality Check

Whenever you assign a state variable array or struct to a local memory variable, Solidity performs a complete deep copy of all the elements from the blockchain's hard drive to the validator's RAM memory slots. This deep copy wastes a massive amount of gas. If you only want to read or update a state variable in place, always reference it as a storage pointer to save gas!

— Production Engineering Principle

5. Code Comparison: The Mutation Bug vs The Pointer Fix

Let's look at the exact code configurations:

SmartAccount.sol
solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
 
contract UserRegistry {
    struct User {
        string username;
        bool isActive;
    }
 
    User[] public users;
 
    function addUser(string memory _name) public {
        users.push(User(_name, false));
    }
 
    // ❌ BROKEN: The status change does NOT persist!
    function deactivateUserMemory(uint256 _index) public {
        User memory tempUser = users[_index]; // Copies users[_index] to temporary memory
        tempUser.isActive = false; // Only updates the temporary local copy!
    }
 
    // ✅ CORRECT: The status change is written permanently
    function deactivateUserStorage(uint256 _index) public {
        User storage pointerUser = users[_index]; // Creates a pointer to slot location
        pointerUser.isActive = false; // Directly modifies the state!
    }
}
  • The Copy Disaster (deactivateUserMemory): The assignment User memory tempUser copies data into the ephemeral RAM stack. The modification of isActive changes the RAM cell but leaves the persistent SSD ledger untouched.
  • The Pointer Optimization (deactivateUserStorage): The assignment User storage pointerUser behaves like a reference link. Modifying it writes to the original array position directly via an active state connection.

// I Got This Wrong

The Calldata Optimization Mistake: A common beginner mistake is declaring dynamic function parameters as memory when they are never modified inside the function body (e.g. checking a whitelist array inside a validation check). If you only need to read a parameter, always declare it as calldata. This tells the EVM to read directly from the transaction payload, skipping the memory copy entirely and saving up to 30% in gas fees!

— Postmortem Confession

System Design Challenge
Think Active

Deploy the UserRegistry contract in Remix. Add a user. Call deactivateUserMemory on index 0, then read the user back. Notice that isActive remains true. Now call deactivateUserStorage on index 0 and verify that it updates correctly!

[ Think Before Continuing ]

Was this lesson helpful?

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