Storage is the single most expensive thing in a smart contract. Writing a new value to the blockchain costs over 20,000 gas, while basic arithmetic costs 3 to 5. If you understand how storage works, you can design contracts that cost a fraction of what a naive implementation would. If you do not, every user interaction burns money unnecessarily.
What is smart contract storage, in plain English
Think of smart contract storage as a warehouse where every shelf costs money to rent, and you can never stop paying. When you declare a state variable in Solidity, you are reserving a permanent shelf in that warehouse. Every node on the Ethereum network stores a copy of that shelf and its contents. That redundancy across thousands of computers is what makes the data tamper-proof, and it is also what makes it expensive.
State variables are the data your contract remembers between function calls. A token balance, an owner address, a mapping of who approved whom: these all live in storage. They persist after the transaction ends, after the block is mined, and for as long as the contract exists on the network.
Temporary variables are different. When you declare a local variable inside a function, it exists only for the duration of that function call. Once the function finishes, the variable is gone. No permanent shelf, no ongoing cost, no network-wide replication.
This is what Solidity calls “memory,” and it is dramatically cheaper than storage.
Why this matters for your project
Cost. A single storage write (setting a value for the first time) costs 20,000 gas. Updating an existing value costs 5,000 gas. Reading from storage costs 2,100 gas. Compare that to basic arithmetic at 3 to 5 gas per operation. If your contract writes ten state variables per transaction, you are spending over 200,000 gas on storage alone. At scale, the difference between thoughtful storage design and careless design can be thousands of dollars per month.
Security. The private keyword in Solidity does not mean what most people think. Marking a variable private only prevents other contracts from reading it through Solidity code. Anyone with a blockchain node can call eth_getStorageAt and read the raw value of any storage slot, including private ones. If your contract stores a password, a secret key, or any sensitive value in storage, it is readable by the entire world. The private keyword controls code-level access, not data visibility.
Architecture. Not everything belongs on-chain. User profile images, large text blobs, and historical logs are better stored off-chain (in IPFS, a database, or event logs) with only a hash or reference kept in storage. The question “does this data need to survive between function calls, and does every node on the network need to verify it?” should guide every storage decision you make.
How it works (the 30-second version)
Every smart contract has access to a massive virtual storage area: 2^256 slots, each holding 32 bytes. State variables are assigned to these slots sequentially, starting at slot 0. The first variable you declare gets slot 0, the second gets slot 1, and so on.
Smaller types can share a slot. If you declare a uint128 followed by another uint128, the Solidity compiler packs both into a single 32-byte slot. But if you declare uint128, then uint256, then another uint128, the compiler uses three separate slots because the uint256 needs a full slot to itself. Ordering your variables by size can cut storage costs significantly.
Mappings work differently. A mapping does not store its data in a sequential slot. Instead, it reserves a slot as a marker, and each key-value pair is stored at a location calculated by hashing the key with the marker slot number using keccak256.
This means mappings can grow without limit and never collide with other variables. It also means you cannot iterate over a mapping, because the keys are scattered across the enormous 2^256 address space.
| Storage type | Solidity keyword | Lifetime | Gas cost | Use case |
|---|---|---|---|---|
| Storage | State variables | Permanent | 20,000 (new), 5,000 (update) | Balances, ownership, settings |
| Memory | Local variables | Single function call | ~3 gas per operation | Temporary calculations |
| Calldata | Function parameters | Single function call | Very cheap to read | External function inputs |
Here is what the distinction looks like in code:
contract Example {
// Storage: permanent, costs 20,000 gas to set initially
uint256 public totalSupply;
mapping(address => uint256) public balances;
function mint(uint256 amount) public {
// Memory: temporary, costs ~3 gas per operation
uint256 newSupply = totalSupply + amount;
// Writing back to storage: costs 5,000 gas (update)
totalSupply = newSupply;
balances[msg.sender] += amount;
}
} The variable newSupply exists only during the mint function call. It is cheap to create and use. But totalSupply and balances persist on-chain forever, and every write to them costs thousands of gas.
How Doodledapp makes this visual
In Doodledapp, the distinction between permanent and temporary data is visible at a glance on your canvas.
State Variable nodes are root nodes. They sit outside of any function flow, have no execution handles, and represent permanent on-chain storage. You configure the type (uint256, address, bool, etc.), toggle public visibility, set a name, and optionally provide a default value.
When you see a State Variable node on the canvas, you are looking at data that will cost 20,000 gas to write for the first time.
Create Variable nodes are work nodes. They live inside function flows, connected to the execution chain with triangular handles. They represent temporary memory that exists only while the function runs. When the function completes, the variable is gone. This visual placement, inside versus outside the flow, tells you immediately whether data is permanent or temporary.
Mapping nodes let you define key-value lookup tables with configurable key and value types. They connect into the execution flow and pair with Get Variable and Set Variable nodes to read and write individual entries.
| Solidity concept | Doodledapp equivalent |
|---|---|
uint256 public balance; | State Variable node (persistent, sits outside functions) |
uint256 temp = x + y; | Create Variable node (temporary, inside function flow) |
mapping(address => uint256) | Mapping node with key/value types |
balances[msg.sender] | Get Variable node with key from Msg.Sender |
balances[msg.sender] = amount | Set Variable node with key and value |
items.push(newItem) | Push node connected in execution flow |
The visual distinction is deliberate. State Variables are root nodes with no exec handles because they represent declarations, not actions. Create Variables are work nodes with exec handles because they are steps in a function’s logic.
You can scan your canvas and immediately identify which data persists on-chain (costing gas on every write) and which data is temporary (cheap and disposable). That spatial awareness helps you make better architecture decisions before you ever compile.
Common mistakes to avoid
Storing too much on-chain. Every 32-byte storage slot costs over 20,000 gas to write for the first time. If you are storing user profile data, long strings, or anything that does not need consensus-level verification, move it off-chain and store only a hash or identifier in the contract.
Assuming “private” means hidden. All storage on the blockchain is publicly readable. Anyone can use eth_getStorageAt to read the value in any storage slot of any contract, regardless of the private keyword. Never store passwords, API keys, or secrets in a smart contract. The private keyword only prevents other Solidity contracts from accessing the variable through code.
Using unbounded arrays for lookups. If your contract loops through an array to find a value, the gas cost grows linearly with the array size. Eventually, the loop will exceed the block gas limit and the function becomes permanently uncallable. Use mappings for key-based lookups instead. They provide constant-time access regardless of how many entries exist.
Ignoring variable ordering. The Solidity compiler packs smaller types into shared 32-byte slots when they are declared next to each other. Declaring uint128, uint128, uint256 uses two slots. Declaring uint128, uint256, uint128 uses three. Group your smaller variables together to save storage slots and reduce gas costs.
The bottom line
Storage is where gas costs add up fastest. Every state variable is a permanent, globally replicated piece of data that costs real money to write and update. Understanding the difference between storage, memory, and calldata, and designing your contracts around that understanding, is the most direct path to contracts that are affordable to use and efficient at scale.