---
title: "Why Storing One Number Costs $50: The State vs Storage Mystery Solved"
description: "Understanding the difference between state and storage in Ethereum, why it matters for gas costs, and how to write efficient smart contracts"
tags: ethereum, state, storage, gas-optimization, smart-contracts, state-trie, evm
---
# Why Storing One Number Costs $50: The State vs Storage Mystery Solved
## Abstract
Ethereum developers constantly confuse "**state**" and "**storage**", using them interchangeably when they're fundamentally different concepts. This confusion leads to expensive smart contracts, wasted gas, and poor user experience.
This article breaks down what state actually is (the global view of all accounts), what storage is (data within smart contracts), how the state trie works, why writing to storage costs 20,000 gas while reading is almost free, and how real projects like Uniswap, Aave, and ERC-20 tokens optimize around these constraints. Through simple analogies and real code examples, you'll understand why storage is expensive, how to minimize costs, and how to write gas-efficient contracts that users can actually afford to interact with.
:::info
**What you'll learn:**
- State vs Storage (they're NOT the same!)
- How Ethereum stores data (the state trie)
- Why storage costs 20,000 gas
- Reading vs writing gas costs
- Real optimization techniques from Uniswap, Aave
- Common mistakes developers make
- How to audit your contracts for gas efficiency
:::
---
## Introduction: The $50 Variable
Once, I built my very first smart contract.
It was a simple voting dApp. The logic was dead simple:
```solidity
contract Voting {
mapping(address => bool) public hasVoted;
mapping(uint256 => uint256) public votes;
function vote(uint256 optionId) public {
require(!hasVoted[msg.sender], "Already voted");
hasVoted[msg.sender] = true; // Mark as voted
votes[optionId] += 1; // Increment vote count
}
}
```
I thought it looked perfect. I deployed it and waited for the first user to vote.
Then it happened.
Gas cost: 43,000 gas
At 50 gwei and $2,000 ETH: $4.30 per vote
The user asked: "Why does voting cost $4.30?!"
I stammered: "Uh… gas?"
They laughed: "I can vote on Twitter for FREE!" and left.
I was devastated.
So I started digging to understand why it was so expensive.
The culprit? Storage operations.
Those two seemingly harmless lines:
Those two innocent lines:
```solidity
hasVoted[msg.sender] = true; // 20,000 gas!
votes[optionId] += 1; // 5,000 gas!
```
…were silently draining money from every voter. That was the day I learned about the $50 variable.
**25,000 gas just for writing two variables!**
That's when I learned the difference between **state** and **storage**.
And once I understood it, I rewrote my contract:
```solidity
// Optimized version - same functionality
contract VotingOptimized {
mapping(address => uint256) public votedOption; // Combines both!
mapping(uint256 => uint256) public votes;
function vote(uint256 optionId) public {
require(votedOption[msg.sender] == 0, "Already voted");
votedOption[msg.sender] = optionId; // 20,000 gas (NEW slot)
votes[optionId] += 1; // 5,000 gas (UPDATE slot)
}
}
```
**New gas cost: 25,000 gas → $2.50 per vote**
Still not free, but **42% cheaper!**
How? By understanding what state and storage actually are.
Let me show you what I learned. By the end of this article, you'll understand:
- What state vs storage means
- Why storage is expensive
- How to optimize your contracts
- Real techniques from production dApps
---
## What Is State?
State is the **global snapshot** of everything on Ethereum at a given moment.
**The Filing Cabinet Analogy:**
Imagine Ethereum as a giant filing cabinet, where the entire cabinet represents Ethereum’s state. Each drawer is an individual account’s state:
for example, Alice’s drawer might show a balance of 5 ETH and a nonce of 42, Bob’s drawer might have 10 ETH and a nonce of 7, and a Uniswap contract’s drawer could contain 100 ETH, its code, and all its storage data. There are millions of such drawers, and together they make up the complete state of Ethereum.
**What's in each account's state:**
```javascript
Account State = {
balance: "Amount of ETH",
nonce: "Transaction count",
codeHash: "Hash of smart contract code (if contract)",
storageRoot: "Hash pointing to storage data"
}
Example - Your wallet:
{
balance: 2.5 ETH,
nonce: 15, // You've sent 15 transactions
codeHash: empty, // Not a contract
storageRoot: empty // No storage
}
Example - Uniswap contract:
{
balance: 100 ETH,
nonce: 1,
codeHash: 0xabc123..., // The Uniswap code
storageRoot: 0xdef456... // Points to ALL Uniswap data
}
```
**Key point: State is the TOP LEVEL view of everything.**
---
## What Is Storage? (Contract Data)
Storage is **data inside smart contracts**, the variables you declare in your Solidity code.
**The Warehouse Analogy:**
If the Ethereum state is a giant filing cabinet, then storage is the warehouse behind each contract’s drawer.
For example, a Uniswap contract’s drawer might show its balance, nonce, and code, but its storage root points to a warehouse that holds all the contract’s variables: factory address, total liquidity pools, fee settings, individual pool addresses, and thousands of other slots, everything the contract needs to function. In short, storage contains all the contract’s variables.
**What goes in storage:**
```solidity
contract MyContract {
// ALL of these are STORAGE variables:
uint256 public totalSupply; // Slot 0
address public owner; // Slot 1
mapping(address => uint256) balances; // Slots 2+
// These are NOT storage (they're in code):
function transfer() public { ... }
// These are NOT storage (they're temporary):
function doSomething() public {
uint256 tempVariable = 42; // Memory, not storage!
}
}
```
**The key difference:**
**STATE:**
- Account-level info
- Balance, nonce, code
- 4 fields per account
- Managed by protocol
- You can't control it directly
**STORAGE:**
- Contract-level data
- Your variables
- Unlimited slots (practically)
- You manage it
- You pay for every write!
---
## The Confusion: Why People Mix Them Up 🤔
Here's where developers get confused:
**The misleading term:**
```solidity
// Solidity calls these "state variables"
contract MyContract {
uint256 public myStateVariable; // Actually STORAGE!
}
// This naming is MISLEADING!
// It's stored in STORAGE, not STATE
// Should be called "storage variable"
```
**What's actually happening:**
```yaml
Your contract is deployed:
STATE (account info):
balance: 0 ETH
nonce: 1
codeHash: 0xabc123...
storageRoot: 0xdef456... ← Points to storage!
STORAGE (your variables):
Slot 0: myStateVariable = 42
Slot 1: anotherVariable = 100
...
The storageRoot in STATE points to STORAGE!
They're separate but connected!
```
**The mental model:**
```
STATE = The container
STORAGE = What's inside the container
Like:
- Car (state) vs contents of trunk (storage)
- Phone (state) vs apps/files (storage)
- House (state) vs furniture (storage)
You can't have storage without state,
but you can have state without storage
(like your wallet - has state but no storage!)
```
---
## How The State Trie Works
Ethereum uses a **Merkle Patricia Trie** to store state. Don't worry, I'll explain simply!
### The Tree Structure
**What's a trie?**
```
Imagine organizing words in a tree:
Start:
├─ 'c'
│ ├─ 'a'
│ │ └─ 't' → CAT ✓
│ └─ 'u'
│ └─ 'p' → CUP ✓
└─ 'd'
└─ 'o'
└─ 'g' → DOG ✓
Each path = one word
Efficient for searching!
```
**Ethereum's state trie:**
```
Instead of words, Ethereum stores accounts:
Root Hash: 0xabcdef...
├─ Hash path → Alice's account
│ {balance: 5 ETH, ...}
│
├─ Hash path → Bob's account
│ {balance: 10 ETH, ...}
│
└─ Hash path → Uniswap contract
{balance: 100 ETH, storage: ...}
Each account = one leaf in the tree
```
### Why Use a Trie?
Benefits:
1. Efficient lookups:
Want Alice's balance?
- Follow hash path to Alice
- O(log n) time
- Fast even with millions of accounts!
2. Cryptographic proof:
Change one account?
- Recalculates hash up to root
- Root hash changes
- Can prove account state
- Light clients can verify!
3. Saves space:
- Shared prefixes stored once
- Efficient for Ethereum's 200M+ accounts
### The Storage Trie
**Each contract has its OWN storage trie:**
```
Uniswap Contract:
State (in state trie):
storageRoot: 0x123abc... ← Points to storage trie!
Storage Trie:
Root: 0x123abc...
├─ Slot 0 → Factory address
├─ Slot 1 → Total pools
├─ Slot 2 → Fee config
└─ Slot 3+ → Pool data
A trie within a trie!
Like Russian dolls 🪆
```
**The full picture:**
```mermaid
graph TD
A[State Root - Block Header] --> B[State Trie]
B --> C[Alice's Account]
B --> D[Bob's Account]
B --> E[Uniswap Contract]
B --> F[AAVE Contract]
E --> E_storage[Storage Trie - Uniswap]
E_storage --> E_slot0[Slot 0]
E_storage --> E_slot1[Slot 1]
E_storage --> E_slot2[Slot 2]
F --> F_storage[Storage Trie - AAVE]
F_storage --> F_slot0[Slot 0]
F_storage --> F_slot1[Slot 1]
```
---
## Gas Costs: Why Storage Is Expensive
Now let's talk about **why this matters for gas costs**:
### Storage Operations (SSTORE)
```yaml
Writing to storage (**SSTORE**):
New value (cold storage):
Cost: 20,000 gas
Why: Must update storage trie
Example: First time setting a variable
Update existing value (**warm**):
Cost: 5,000 gas
Why: Trie already has entry, just update
Example: Incrementing a counter
Setting to zero (refund!):
Cost: 5,000 gas
Refund: 15,000 gas
Net: -10,000 gas (you get refunded!)
Why: Cleaning up storage is good!
```
**Real example:**
```solidity
contract GasCosts {
uint256 public myNumber;
function firstWrite() public {
myNumber = 42;
// Gas: 20,000 (cold SSTORE)
}
function updateWrite() public {
myNumber = 100;
// Gas: 5,000 (warm SSTORE)
}
function deleteWrite() public {
myNumber = 0;
// Gas: 5,000 - 15,000 refund = -10,000!
}
}
```
### Reading from Storage (SLOAD)
```yaml
Reading storage (SLOAD):
Cold read (first time):
Cost: 2,100 gas
Why: Must traverse storage trie
Warm read (subsequent):
Cost: 100 gas
Why: Already cached in memory
Much cheaper than writing!
```
**Real example:**
```solidity
function readExample() public view returns (uint256) {
uint256 a = myNumber; // 2,100 gas (cold)
uint256 b = myNumber; // 100 gas (warm)
uint256 c = myNumber; // 100 gas (warm)
return a + b + c;
// Total: 2,300 gas to read same variable 3 times
}
```
### Memory vs Storage
**Memory (temporary):**
Cost: 3 gas per 32 bytes
Scope: Function call only
Persists: No (gone after function ends)
**Storage (permanent):**
Cost: 20,000 gas first write
Scope: Forever
Persists: Yes (stays on-chain forever)
*The difference: 6,666x more expensive!*
**Visual comparison:**
```mermaid
graph TD
A[Writing 32 bytes] --> B[To Memory]
A --> C[To Storage]
B --> B1[Cost: 3 gas]
B --> B2[Speed: Instant]
B --> B3[Lasts: Until function ends]
C --> C1[Cost: 20,000 gas]
C --> C2[Speed: Same]
C --> C3[Lasts: Forever]
C1 --> D[Storage is 6,666x more expensive!]
```
---
## Real-World Gas Costs
Let me show you actual costs:
```solidity
contract GasExample {
// Storage variables (expensive!)
uint256 public storageNumber;
mapping(address => uint256) public balances;
function expensiveOperations() public {
// 1. Write to NEW storage slot
storageNumber = 42;
// Cost: 20,000 gas ($1.20 at 50 gwei, $2000 ETH)
// 2. Update EXISTING storage
storageNumber += 1;
// Cost: 5,000 gas ($0.30)
// 3. Write to mapping (NEW key)
balances[msg.sender] = 100;
// Cost: 20,000 gas ($1.20)
// 4. Update mapping (EXISTING key)
balances[msg.sender] += 50;
// Cost: 5,000 gas ($0.30)
// TOTAL: 50,000 gas = $3.00
}
function cheapOperations() public view {
// 1. Read from storage
uint256 x = storageNumber;
// Cost: 2,100 gas ($0.13)
// 2. Read again (warm)
uint256 y = storageNumber;
// Cost: 100 gas ($0.006)
// 3. Use memory
uint256 z = x + y;
// Cost: 3 gas ($0.0002)
// TOTAL: 2,203 gas = $0.13
// 23x cheaper than writing!
}
}
```
**Real transaction costs:**
```yaml
At current gas prices (50 gwei, ETH $2,000):
Simple ETH transfer:
Gas: 21,000
Cost: $2.10
ERC-20 transfer (2 storage updates):
Gas: ~50,000
Cost: $5.00
Why: Updates sender & receiver balances
Uniswap swap (multiple storage updates):
Gas: ~150,000
Cost: $15.00
Why: Updates reserves, user balance, fees
NFT mint (several storage writes):
Gas: ~80,000
Cost: $8.00
Why: Sets owner, updates counter, emits event
The pattern: More storage writes = More expensive!
```
---
## Storage Layout: How Solidity Stores Variables
Understanding storage layout helps you optimize:
### Storage Slots
```solidity
contract StorageLayout {
// Each storage variable gets a "slot"
// Slots are 32 bytes (256 bits)
uint256 a; // Slot 0 (takes full slot)
uint256 b; // Slot 1 (takes full slot)
uint128 c; // Slot 2 (takes half slot)
uint128 d; // Slot 2 (shares with c! Packed!)
address owner; // Slot 3 (20 bytes, wastes 12)
bool isActive; // Slot 3 (shares with owner! Packed!)
// Mappings and arrays are special
mapping(address => uint256) balances; // Slot 4 (stores position)
uint256[] numbers; // Slot 5 (stores length)
}
```
**How slots are used:**
```yaml
Slot 0: [ a (32 bytes) ]
Slot 1: [ b (32 bytes) ]
Slot 2: [ c (16 bytes) ][ d (16 bytes) ] ← Packed!
Slot 3: [ owner (20b) ][isActive(1b)][unused(11b)] ← Packed!
Slot 4: [ balances mapping position ]
Slot 5: [ numbers array length ]
Packing saves slots = Saves gas!
```
### Bad Layout (Wastes Gas)
```solidity
contract BadLayout {
uint128 a; // Slot 0 (half slot)
uint256 b; // Slot 1 (needs full slot, can't share)
uint128 c; // Slot 2 (half slot, wasted space in slot 0!)
// Total: 3 slots used
// Gas to initialize: 60,000 (3 x 20,000)
}
```
### Good Layout (Saves Gas)
```solidity
contract GoodLayout {
uint128 a; // Slot 0 (half slot)
uint128 c; // Slot 0 (shares with a!)
uint256 b; // Slot 1 (full slot)
// Total: 2 slots used
// Gas to initialize: 40,000 (2 x 20,000)
// Saved: 20,000 gas ($1.20!)
}
```
### Mappings Are Special
```solidity
contract MappingExample {
mapping(address => uint256) balances; // Declared in slot 0
// But data NOT stored in slot 0!
// Slot 0 just marks "there's a mapping here"
// Actual data location:
// For key 0xAlice:
// slot = keccak256(abi.encode(0xAlice, 0))
// = some hash like 0x7f3a89...
// For key 0xBob:
// slot = keccak256(abi.encode(0xBob, 0))
// = different hash like 0x2b9c45...
// Each key gets unique slot via hashing!
// No collisions possible!
}
```
**Why this matters:**
Mapping storage:
- Each key-value pair = separate slot
- Writing new key = 20,000 gas (new slot)
- Updating existing key = 5,000 gas
- No way to iterate (by design!)
- Very efficient for lookups
---
## Optimization Techniques
Let's learn from real projects:
### Technique 1: Pack Variables (Uniswap V2)
**Bad code:**
```solidity
contract BadPair {
uint256 reserve0; // Slot 0
uint256 reserve1; // Slot 1
uint256 blockTimestampLast; // Slot 2
// 3 slots = 60,000 gas to write all
}
```
**Uniswap's optimized code:**
```solidity
contract UniswapV2Pair {
uint112 reserve0; // Slot 0
uint112 reserve1; // Slot 0 (packed!)
uint32 blockTimestampLast; // Slot 0 (packed!)
// 1 slot = 20,000 gas to write all
// Saved: 40,000 gas!
// Why this works:
// - Reserves don't need 256 bits (112 is enough)
// - Timestamp only needs 32 bits (year 2106 ok!)
// - 112 + 112 + 32 = 256 bits = 1 slot!
}
```
**Savings:**
```yaml
Per liquidity pool update:
Bad: 60,000 gas
Uniswap: 20,000 gas
Savings: 40,000 gas ($2.40)
With millions of swaps:
Savings: Millions of dollars!
```
---
### Technique 2: Use Events Instead of Storage (ERC-20)
**Bad code:**
```solidity
contract BadToken {
mapping(address => uint256[]) public transferHistory;
function transfer(address to, uint256 amount) public {
balances[msg.sender] -= amount;
balances[to] += amount;
// Store history (EXPENSIVE!)
transferHistory[msg.sender].push(amount);
// Cost: 20,000 gas per transfer!
}
}
```
**ERC-20's optimized approach:**
```solidity
contract ERC20 {
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address to, uint256 amount) public {
balances[msg.sender] -= amount;
balances[to] += amount;
// Emit event instead of storing!
emit Transfer(msg.sender, to, amount);
// Cost: ~1,500 gas (13x cheaper!)
}
}
```
**Why events are cheaper:**
Storage:
- Stored in state trie forever
- Accessible by smart contracts
- Cost: 20,000 gas per write
Events:
- Stored in logs (separate)
- NOT accessible by contracts
- Queryable off-chain
- Cost: ~375 gas per log topic
*Use events for historical data you query off-chain!*
**Real impact:**
```yaml
USDC contract:
- Billions of transfers
- All use events, not storage
- If they used storage:
→ 20,000 extra gas per transfer
→ $12 extra per transfer
→ Billions wasted!
By using events:
→ Transfer: ~$3
→ Not $15!
→ USDC actually usable! ✓
```
---
### Technique 3: Batch Operations (Aave)
**Bad code:**
```solidity
contract BadLending {
function depositMultiple(
address[] tokens,
uint256[] amounts
) public {
for (uint i = 0; i < tokens.length; i++) {
// Update storage in loop (expensive!)
deposits[msg.sender][tokens[i]] = amounts[i];
// Each iteration: 20,000 gas
}
}
}
```
**Aave's approach:**
```solidity
contract AavePool {
function supply(
address asset,
uint256 amount,
address onBehalfOf,
uint16 referralCode
) public {
// Read from storage once
DataTypes.ReserveData memory reserve =
_reserves[asset];
// Do all calculations in memory (cheap!)
uint256 newBalance = reserve.balance + amount;
uint256 newIndex = calculateIndex(reserve);
// Write back to storage once (expensive)
_reserves[asset].balance = newBalance;
_reserves[asset].index = newIndex;
// Pattern: Read once, calculate in memory, write once
}
}
```
**The pattern:**
```yaml
Bad:
Read storage → Calculate → Write
Read storage → Calculate → Write
Read storage → Calculate → Write
Cost: Many expensive operations
Good:
Read storage once → Calculate in memory → Write once
Cost: Minimal storage operations
```
---
### Technique 4: Use Smaller Types (OpenZeppelin)
**Bad code:**
```solidity
contract BadNFT {
mapping(uint256 => address) public owners;
mapping(uint256 => bool) public exists;
uint256 public totalSupply;
function mint(address to, uint256 tokenId) public {
owners[tokenId] = to; // 20,000 gas
exists[tokenId] = true; // 20,000 gas
totalSupply += 1; // 5,000 gas
// Total: 45,000 gas per mint!
}
}
```
**OpenZeppelin's ERC-721 optimization:**
```solidity
contract ERC721 {
mapping(uint256 => address) private _owners;
// Removed exists mapping!
function mint(address to, uint256 tokenId) internal {
_owners[tokenId] = to; // 20,000 gas
// If owner != address(0), token exists!
// Saved: 20,000 gas!
// Total: 20,000 gas per mint
// 55% cheaper!
}
function exists(uint256 tokenId) public view returns (bool) {
return _owners[tokenId] != address(0);
// Gas: 2,100 (read only)
// vs 20,000 to write bool!
}
}
```
**The insight:**
```yaml
Instead of storing:
owners[tokenId] = address
exists[tokenId] = bool ← Redundant!
Just check:
owners[tokenId] != address(0)
Saved one mapping = Saved 20,000 gas per mint!
```
### Technique 5: Immutable Variables
**Bad code:**
```solidity
contract BadContract {
address public owner; // Storage (mutable)
uint256 public fee; // Storage (mutable)
constructor(address _owner, uint256 _fee) {
owner = _owner; // 20,000 gas
fee = _fee; // 20,000 gas
}
function withdraw() public {
require(msg.sender == owner); // 2,100 gas (SLOAD)
// ...
}
}
```
**Optimized code:**
```solidity
contract GoodContract {
address public immutable owner; // In bytecode, not storage!
uint256 public immutable fee;
constructor(address _owner, uint256 _fee) {
owner = _owner; // Compiled into bytecode
fee = _fee; // Not stored in storage!
}
function withdraw() public {
require(msg.sender == owner); // 0 gas (loaded from code!)
// ...
}
}
```
**Immutable vs Storage:**
Storage variable:
- Write: 20,000 gas
- Read: 2,100 gas
- Can change after deployment
Immutable:
- Set: No storage cost (in bytecode)
- Read: No gas (loaded from code)
- Cannot change after deployment
*Use immutable for constants set in constructor!*
---
## Common Mistakes & How to Fix Them
### Mistake 1: Storing What You Can Calculate
**Bad:**
```solidity
contract BadMath {
uint256 public total;
uint256[] public numbers;
function addNumber(uint256 num) public {
numbers.push(num); // 20,000 gas
total += num; // 5,000 gas
// Storing total is redundant!
// Total: 25,000 gas
}
}
```
**Good:**
```solidity
contract GoodMath {
uint256[] public numbers;
function addNumber(uint256 num) public {
numbers.push(num); // 20,000 gas
// Don't store total!
}
function total() public view returns (uint256) {
uint256 sum = 0;
for (uint i = 0; i < numbers.length; i++) {
sum += numbers[i]; // Memory operation (cheap!)
}
return sum;
// Gas: 2,100 + (100 * array length)
// Still cheaper than storing!
}
}
```
---
### Mistake 2: Not Using Memory
**Bad:**
```solidity
contract BadLoop {
uint256[] public items;
function processItems() public {
for (uint i = 0; i < items.length; i++) {
uint256 item = items[i]; // SLOAD every iteration!
// 2,100 gas per item
// Process item...
}
}
}
```
**Good:**
```solidity
contract GoodLoop {
uint256[] public items;
function processItems() public {
uint256[] memory itemsCopy = items; // Load once to memory
// Cost: 2,100 + (3 * items.length)
for (uint i = 0; i < itemsCopy.length; i++) {
uint256 item = itemsCopy[i]; // Memory read (3 gas)
// Process item...
}
// If items.length > 700, this is cheaper!
}
}
```
---
### Mistake 3: Redundant Storage Updates
**Bad:**
```solidity
contract BadCounter {
uint256 public counter;
function increment() public {
counter = counter + 1; // Read + Write = 2,100 + 5,000 = 7,100 gas
}
}
```
**Good:**
```solidity
contract GoodCounter {
uint256 public counter;
function increment() public {
counter++; // Compiler optimizes this!
// Solidity is smart about this pattern
// Gas: 5,000 (just the write)
}
}
```
---
### Mistake 4: Over-Using Mappings
**Bad:**
```solidity
contract BadRegistry {
mapping(address => string) public names;
mapping(address => uint256) public ages;
mapping(address => bool) public isActive;
function register(string memory name, uint256 age) public {
names[msg.sender] = name; // 20,000+ gas (string)
ages[msg.sender] = age; // 20,000 gas
isActive[msg.sender] = true; // 20,000 gas
// Total: 60,000+ gas!
}
}
```
**Good:**
```solidity
contract GoodRegistry {
struct User {
string name;
uint256 age;
bool isActive;
}
mapping(address => User) public users;
function register(string memory name, uint256 age) public {
users[msg.sender] = User(name, age, true);
// Single mapping entry!
// Total: ~25,000 gas
// Saved: 35,000 gas!
}
}
```
---
## How to Audit Your Contracts for Gas
### Use Hardhat Gas Reporter
```javascript
// hardhat.config.js
require("hardhat-gas-reporter");
module.exports = {
gasReporter: {
enabled: true,
currency: 'USD',
coinmarketcap: API_KEY
}
};
```
---
### Check Storage Layout
```bash
npx hardhat compile
# Then inspect build artifacts:
cat artifacts/contracts/MyContract.sol/MyContract.json
# Look for "storageLayout":
{
"storage": [
{
"astId": 3,
"contract": "MyContract",
"label": "myVar",
"offset": 0,
"slot": "0",
"type": "uint256"
}
]
}
```
---
### Use Gas Optimization Tools
```bash
# 1. Slither (static analyzer)
pip install slither-analyzer
slither . --detect costly-loops
# 2. Echidna (fuzzer)
echidna-test . --test-mode assertion
# 3. Foundry gas snapshots
forge snapshot
forge snapshot --diff
```
## Real Project Comparisons
**Uniswap V2:**
- Optimization: Pack 3 variables into 1 slot
- Variables: uint112 + uint112 + uint32 = 256 bits
- Saves: 40,000 gas per update
- Impact: Millions saved across billions of swaps
**Aave V3:**
- Optimization: Bitmap for user config
- Instead of: 20 bool variables (20 slots)
- Uses: 1 uint256 bitmap (1 slot)
- Saves: 380,000 gas per configuration
- Impact: Configuration actually affordable
**Compound:**
- Optimization: Accrue interest on read, not write
- Instead of: Update on every transaction
- Does: Update only when someone checks
- Saves: 5,000 gas per transaction
- Impact: Cheaper to use
**OpenZeppelin ERC-721:**
Optimization: Removed exists mapping
Instead of: owners + exists mappings
Uses: Just owners (check != address(0))
Saves: 20,000 gas per mint
Impact: NFTs 55% cheaper to mint
## Key Takeaways
Let me summarize state vs storage:
### The Differences
STATE:
- What: Global account info
- Contains: Balance, nonce, code, storageRoot
- Who manages: Protocol
- You control: No (except via transactions)
- Cost: Determined by what you do
STORAGE:
- What: Contract variables
- Contains: Your declared variables
- Who manages: You (the developer)
- You control: Yes (fully)
- Cost: You pay directly (20k gas per slot)
### Gas Costs to Remember
```yaml
Storage operations:
New slot (SSTORE): 20,000 gas
Update slot: 5,000 gas
Delete (set to 0): -10,000 gas (refund!)
Read cold: 2,100 gas
Read warm: 100 gas
Memory operations:
Write: 3 gas per 32 bytes
Read: 3 gas
Events:
Emit: ~375 gas per indexed field
The pattern: Storage > Memory > Events (by cost)
```
### Optimization Strategies
1. Pack variables (uint256 → uint128 x 2)
2. Use events for historical data
3. Use immutable for constants
4. Batch storage operations
5. Avoid redundant data
6. Use structs instead of multiple mappings
7. Load to memory in loops
8. Use smaller types when possible
9. Delete unused storage (get refunds!)
10. Calculate instead of storing when cheap
---
## Conclusion: Every Byte Counts
Remember my $4.30 voting contract?
After understanding state vs storage:
- Packed variables where possible
- Used events for logs
- Removed redundant data
- Optimized storage layout
**Final cost: $1.20 per vote** (74% savings!)
Still not free, but **affordable enough that people actually used it.**
**The lesson:**
Storage is **expensive** on Ethereum. Not because the devs are greedy, but because:
1. Every write updates the global state trie
2. Every node must store this forever
3. Every client must sync it
4. Cryptographic proofs must be maintained
**It SHOULD be expensive.** Otherwise, the chain would be bloated and unusable.
**But that doesn't mean you waste it!**
Good developers:
- Understand state vs storage
- Know the gas costs
- Optimize ruthlessly
- Make dApps affordable
Bad developers:
- Think "state" and "storage" are the same
- Don't check gas costs
- Write inefficient contracts
- Wonder why nobody uses their dApp
**The difference between a successful dApp and a failed one is often just understanding storage costs.**
Uniswap saves millions in gas → People use it → Success
Your unoptimized DEX costs 3x more → Nobody uses it → Failure
**It really is that simple.**
So next time you write:
```solidity
uint256 public myVariable; // "State variable" they call it
```
Remember: That's actually **STORAGE**. And it costs 20,000 gas to write.
Every. Single. Time.
Make it count.
---
**Want to learn more?**
Official resources:
- [Ethereum Yellow Paper](https://ethereum.github.io/yellowpaper/paper.pdf) - State trie specification
- [Solidity Storage Layout](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html) - Official docs
- [EVM Opcodes](https://www.evm.codes/) - Gas costs for each operation
Tools:
- [Hardhat Gas Reporter](https://www.npmjs.com/package/hardhat-gas-reporter)
- [Foundry](https://book.getfoundry.sh/) - Gas optimization tools
- [Slither](https://github.com/crytic/slither) - Static analyzer
Optimization guides:
- [OpenZeppelin Gas Optimization](https://docs.openzeppelin.com/contracts/4.x/api/utils#Address)
- [Solidity Patterns](https://fravoll.github.io/solidity-patterns/)
**P.S.** The next time someone says "store that in state," politely correct them: "You mean storage?" Then watch them realize they've been confused this whole time. 😊
**P.P.S.** Every gas you save is a dollar saved for your users. Optimize like your product depends on it, because it does!
---
###### tags: `ethereum` `state` `storage` `gas-optimization` `smart-contracts` `solidity` `evm` `state-trie`