[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-2-memory-model-stack-storage-calldata":3},{"article":4,"author":54},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":7,"meta_description":16,"focus_keyword":17,"og_image":18,"canonical_url":18,"robots_meta":19,"created_at":15,"updated_at":15,"tags":20,"category_name":34,"related_articles":35},"d0000000-0000-0000-0000-000000000102","a0000000-0000-0000-0000-000000000002","Deep EVM #2: Memory Model — Stack, Memory, Storage, and Calldata","deep-evm-2-memory-model-stack-storage-calldata","A deep dive into the EVM's four data locations — stack, memory, storage, and calldata — their costs, behavior, and the memory expansion formula that catches developers off guard.","## Four Places Your Data Can Live\n\nThe EVM provides four distinct data locations, each with radically different costs, lifetimes, and access patterns. Choosing the wrong one is the single most common source of excessive gas consumption in smart contracts.\n\n| Location | Lifetime | Read Cost | Write Cost | Size |\n|----------|----------|-----------|------------|------|\n| Stack | Current opcode | 0 (DUP: 3) | 0 (PUSH: 3) | 1024 words |\n| Memory | Current call context | MLOAD: 3* | MSTORE: 3* | Expandable |\n| Storage | Permanent (blockchain) | SLOAD: 2100\u002F100 | SSTORE: 20000\u002F2900\u002F100 | 2^256 slots |\n| Calldata | Current call context | CALLDATALOAD: 3 | Read-only | Transaction input |\n\n*Memory costs 3 gas per operation plus a quadratic expansion cost.\n\n## The Stack: Fast but Tiny\n\nThe stack is the EVM's working memory. Every arithmetic, comparison, and logical operation reads from and writes to the stack. It holds up to 1024 elements, each 32 bytes wide.\n\nIn practice, you rarely use more than 16 stack slots because DUP and SWAP opcodes only reach 16 elements deep. The Solidity compiler manages stack layout automatically, but in Yul and Huff you manage it manually — and \"stack too deep\" errors become your constant companion.\n\nStack access is essentially free (3 gas for PUSH\u002FDUP\u002FSWAP operations). This makes the stack the ideal location for intermediate computations, loop counters, and temporary values.\n\n```yul\n\u002F\u002F Stack-only computation: sum of 1 + 2 + 3\n\u002F\u002F Total gas: 3 + 3 + 3 + 3 + 3 = 15 gas\nlet a := add(1, 2)    \u002F\u002F PUSH1 1, PUSH1 2, ADD\nlet b := add(a, 3)    \u002F\u002F DUP1, PUSH1 3, ADD\n```\n\n## Memory: Cheap but Ephemeral\n\nMemory is a byte-addressable array that starts empty and expands as needed. It persists only for the duration of the current call context — when a CALL, STATICCALL, or DELEGATECALL returns, the callee's memory is destroyed.\n\nMemory is accessed in 32-byte chunks:\n- **MSTORE(offset, value)** — Write 32 bytes at the given offset. Costs 3 gas.\n- **MLOAD(offset)** — Read 32 bytes from the given offset. Costs 3 gas.\n- **MSTORE8(offset, value)** — Write a single byte. Costs 3 gas.\n\n### The Free Memory Pointer\n\nSolidity reserves the first 128 bytes of memory for special purposes:\n\n| Offset | Purpose |\n|--------|---------|\n| 0x00-0x3f | Scratch space (used for hashing) |\n| 0x40-0x5f | Free memory pointer |\n| 0x60-0x7f | Zero slot (used as initial value for dynamic memory arrays) |\n| 0x80+ | Free memory (allocation starts here) |\n\nThe **free memory pointer** at `0x40` tracks the next available memory address. When Solidity allocates memory (e.g., `new bytes(100)`, `abi.encode(...)`, creating a struct in memory), it reads the pointer, uses that address, and advances the pointer.\n\n```solidity\n\u002F\u002F What Solidity does internally when you write:\n\u002F\u002F bytes memory data = new bytes(64);\n\n\u002F\u002F 1. Read free memory pointer: MLOAD(0x40) -> 0x80\n\u002F\u002F 2. Store array length at 0x80: MSTORE(0x80, 64)\n\u002F\u002F 3. Update free memory pointer: MSTORE(0x40, 0x80 + 32 + 64 = 0xE0)\n\u002F\u002F 4. Data lives at 0xA0 through 0xDF\n```\n\n### Memory Expansion Cost\n\nHere is where memory gets expensive. The gas cost of memory is not just 3 gas per MLOAD\u002FMSTORE — there is a quadratic expansion cost charged when you access memory beyond the current high-water mark.\n\nThe formula (from the Yellow Paper):\n\n```\nmemory_cost = (memory_size_words^2 \u002F 512) + (3 * memory_size_words)\n```\n\nwhere `memory_size_words = ceil(highest_accessed_byte \u002F 32)`.\n\nFor small memory usage (under ~700 bytes), the cost is approximately linear at 3 gas per word. But it grows quadratically:\n\n| Memory Used | Cost |\n|-------------|------|\n| 32 bytes (1 word) | 3 gas |\n| 1 KB (32 words) | 98 gas |\n| 10 KB (320 words) | 1160 gas |\n| 100 KB (3200 words) | 29600 gas |\n| 1 MB (32000 words) | 2,001,000 gas |\n\nThis quadratic growth means that contracts processing large arrays in memory can hit gas limits quickly. It is also why Solidity uses memory sparingly and prefers stack-based computation where possible.\n\n## Storage: Permanent but Expensive\n\nStorage is the EVM's persistent key-value store. Each contract has its own isolated storage space with 2^256 32-byte slots. Storage persists across transactions — it is the blockchain state.\n\n### Storage Layout in Solidity\n\nSolidity assigns storage slots sequentially to state variables:\n\n```solidity\ncontract StorageLayout {\n    uint256 public x;       \u002F\u002F slot 0\n    uint256 public y;       \u002F\u002F slot 1\n    address public owner;   \u002F\u002F slot 2 (packed: 20 bytes)\n    bool public paused;     \u002F\u002F slot 2 (packed: 1 byte, same slot!)\n    mapping(address => uint256) public balances;  \u002F\u002F slot 3 (base)\n    \u002F\u002F balances[addr] lives at: keccak256(addr . slot_3)\n}\n```\n\nMappings and dynamic arrays use `keccak256` to derive their storage slot:\n- `mapping[key]` -> `keccak256(key || slot_number)`\n- `array[index]` -> `keccak256(slot_number) + index`\n\n### Storage Costs (Post EIP-2929 + EIP-3529)\n\nStorage is by far the most expensive data location:\n\n| Operation | Cold | Warm |\n|-----------|------|------|\n| SLOAD | 2100 | 100 |\n| SSTORE (zero to nonzero) | 22100 | 20000 |\n| SSTORE (nonzero to nonzero) | 5000 | 2900 |\n| SSTORE (nonzero to zero) | 5000 + 4800 refund | 2900 + 4800 refund |\n\nSetting a storage slot from zero to a nonzero value costs **20000 gas** because the Ethereum state trie must insert a new leaf node. This is why initializing a new mapping entry (like a token balance for a new holder) is so expensive.\n\n### Storage Packing\n\nSolidity packs multiple small variables into a single 32-byte slot when possible. This is a critical optimization:\n\n```solidity\n\u002F\u002F BAD: 3 storage slots = 3 x SLOAD = 6300 gas (cold)\ncontract Unpacked {\n    uint256 a;  \u002F\u002F slot 0 (32 bytes)\n    uint256 b;  \u002F\u002F slot 1 (32 bytes)\n    uint256 c;  \u002F\u002F slot 2 (32 bytes)\n}\n\n\u002F\u002F GOOD: 1 storage slot = 1 x SLOAD = 2100 gas (cold)\ncontract Packed {\n    uint64 a;   \u002F\u002F slot 0, bytes 0-7\n    uint64 b;   \u002F\u002F slot 0, bytes 8-15\n    uint64 c;   \u002F\u002F slot 0, bytes 16-23\n}\n```\n\n## Calldata: Read-Only and Cheap\n\nCalldata is the input data sent with a transaction or a call. It is read-only, and accessing it is cheap (3 gas for CALLDATALOAD). For external function calls, Solidity ABI-encodes the arguments into calldata.\n\n```\n\u002F\u002F transfer(address to, uint256 amount)\n\u002F\u002F calldata: 0xa9059cbb\n\u002F\u002F           0000000000000000000000001234567890abcdef1234567890abcdef12345678\n\u002F\u002F           0000000000000000000000000000000000000000000000000de0b6b3a7640000\n\u002F\u002F           ^-- function selector (4 bytes)\n\u002F\u002F                     ^-- address, padded to 32 bytes\n\u002F\u002F                                                                              ^-- uint256 amount\n```\n\nUsing `calldata` instead of `memory` for function parameters avoids copying data and saves significant gas:\n\n```solidity\n\u002F\u002F BAD: copies entire array to memory\nfunction sum(uint256[] memory arr) external returns (uint256) { ... }\n\n\u002F\u002F GOOD: reads directly from calldata\nfunction sum(uint256[] calldata arr) external returns (uint256) { ... }\n```\n\n## Transient Storage (EIP-1153)\n\nEIP-1153 (Cancun upgrade, March 2024) introduced two new opcodes: **TSTORE** and **TLOAD**. Transient storage is like regular storage but it is automatically cleared at the end of the transaction.\n\nKey properties:\n- Same key-value model as storage (2^256 slots of 32 bytes)\n- Costs 100 gas for both TLOAD and TSTORE (similar to warm SLOAD\u002FSSTORE)\n- Cleared after transaction completes — no permanent state change\n- Accessible across call frames within the same transaction\n\nThis is perfect for:\n- **Reentrancy locks** — Set a flag with TSTORE, check with TLOAD. No 20000 gas SSTORE cost.\n- **Cross-contract communication within a transaction** — Callback patterns, flash loans.\n- **Temporary approvals** — ERC-20 temporary allowances within a single transaction.\n\n```solidity\n\u002F\u002F Before EIP-1153: reentrancy lock costs ~5000-20000 gas\nmapping(address => bool) private _locked;\n\n\u002F\u002F After EIP-1153: reentrancy lock costs ~200 gas\nassembly {\n    if tload(0) { revert(0, 0) }  \u002F\u002F check lock\n    tstore(0, 1)                   \u002F\u002F set lock\n}\n\u002F\u002F ... function body ...\nassembly {\n    tstore(0, 0)                   \u002F\u002F clear lock\n}\n```\n\n## Returndata: The Fifth Location\n\nThere is actually a fifth data location: **returndata**. After a CALL, STATICCALL, or DELEGATECALL completes, the callee's return data is available via RETURNDATASIZE and RETURNDATACOPY. This data exists only until the next call opcode overwrites it.\n\n```yul\n\u002F\u002F Read return data after an external call\nlet success := call(gas(), target, 0, 0, 0, 0, 0)\nlet size := returndatasize()\nlet ptr := mload(0x40)\nreturndatacopy(ptr, 0, size)\n```\n\n## Practical Optimization: Choosing the Right Location\n\nHere is a decision framework for MEV bot developers:\n\n1. **Can it live on the stack?** Use the stack. Zero marginal cost.\n2. **Do you need more than 16 values?** Use memory. Allocate from the free memory pointer.\n3. **Is it read-only input data?** Use calldata. 3 gas per 32-byte read.\n4. **Must it persist across transactions?** Use storage. Budget 20000+ gas for new entries.\n5. **Must it persist across call frames but not across transactions?** Use transient storage. 100 gas.\n\n## Conclusion\n\nThe EVM's memory model is deceptively simple — four locations with clear tradeoffs. But the cost differences are enormous: a stack ADD costs 3 gas while an SSTORE costs 20000. Understanding these costs at a visceral level is what separates gas-efficient contracts from gas-burning ones. In the next article, we will quantify gas costs precisely and explore optimization patterns that can save thousands of gas per call.","\u003Ch2 id=\"four-places-your-data-can-live\">Four Places Your Data Can Live\u003C\u002Fh2>\n\u003Cp>The EVM provides four distinct data locations, each with radically different costs, lifetimes, and access patterns. Choosing the wrong one is the single most common source of excessive gas consumption in smart contracts.\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Location\u003C\u002Fth>\u003Cth>Lifetime\u003C\u002Fth>\u003Cth>Read Cost\u003C\u002Fth>\u003Cth>Write Cost\u003C\u002Fth>\u003Cth>Size\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Stack\u003C\u002Ftd>\u003Ctd>Current opcode\u003C\u002Ftd>\u003Ctd>0 (DUP: 3)\u003C\u002Ftd>\u003Ctd>0 (PUSH: 3)\u003C\u002Ftd>\u003Ctd>1024 words\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Memory\u003C\u002Ftd>\u003Ctd>Current call context\u003C\u002Ftd>\u003Ctd>MLOAD: 3*\u003C\u002Ftd>\u003Ctd>MSTORE: 3*\u003C\u002Ftd>\u003Ctd>Expandable\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Storage\u003C\u002Ftd>\u003Ctd>Permanent (blockchain)\u003C\u002Ftd>\u003Ctd>SLOAD: 2100\u002F100\u003C\u002Ftd>\u003Ctd>SSTORE: 20000\u002F2900\u002F100\u003C\u002Ftd>\u003Ctd>2^256 slots\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Calldata\u003C\u002Ftd>\u003Ctd>Current call context\u003C\u002Ftd>\u003Ctd>CALLDATALOAD: 3\u003C\u002Ftd>\u003Ctd>Read-only\u003C\u002Ftd>\u003Ctd>Transaction input\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>*Memory costs 3 gas per operation plus a quadratic expansion cost.\u003C\u002Fp>\n\u003Ch2 id=\"the-stack-fast-but-tiny\">The Stack: Fast but Tiny\u003C\u002Fh2>\n\u003Cp>The stack is the EVM’s working memory. Every arithmetic, comparison, and logical operation reads from and writes to the stack. It holds up to 1024 elements, each 32 bytes wide.\u003C\u002Fp>\n\u003Cp>In practice, you rarely use more than 16 stack slots because DUP and SWAP opcodes only reach 16 elements deep. The Solidity compiler manages stack layout automatically, but in Yul and Huff you manage it manually — and “stack too deep” errors become your constant companion.\u003C\u002Fp>\n\u003Cp>Stack access is essentially free (3 gas for PUSH\u002FDUP\u002FSWAP operations). This makes the stack the ideal location for intermediate computations, loop counters, and temporary values.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Stack-only computation: sum of 1 + 2 + 3\n\u002F\u002F Total gas: 3 + 3 + 3 + 3 + 3 = 15 gas\nlet a := add(1, 2)    \u002F\u002F PUSH1 1, PUSH1 2, ADD\nlet b := add(a, 3)    \u002F\u002F DUP1, PUSH1 3, ADD\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"memory-cheap-but-ephemeral\">Memory: Cheap but Ephemeral\u003C\u002Fh2>\n\u003Cp>Memory is a byte-addressable array that starts empty and expands as needed. It persists only for the duration of the current call context — when a CALL, STATICCALL, or DELEGATECALL returns, the callee’s memory is destroyed.\u003C\u002Fp>\n\u003Cp>Memory is accessed in 32-byte chunks:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>MSTORE(offset, value)\u003C\u002Fstrong> — Write 32 bytes at the given offset. Costs 3 gas.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>MLOAD(offset)\u003C\u002Fstrong> — Read 32 bytes from the given offset. Costs 3 gas.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>MSTORE8(offset, value)\u003C\u002Fstrong> — Write a single byte. Costs 3 gas.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>The Free Memory Pointer\u003C\u002Fh3>\n\u003Cp>Solidity reserves the first 128 bytes of memory for special purposes:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Offset\u003C\u002Fth>\u003Cth>Purpose\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>0x00-0x3f\u003C\u002Ftd>\u003Ctd>Scratch space (used for hashing)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>0x40-0x5f\u003C\u002Ftd>\u003Ctd>Free memory pointer\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>0x60-0x7f\u003C\u002Ftd>\u003Ctd>Zero slot (used as initial value for dynamic memory arrays)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>0x80+\u003C\u002Ftd>\u003Ctd>Free memory (allocation starts here)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>The \u003Cstrong>free memory pointer\u003C\u002Fstrong> at \u003Ccode>0x40\u003C\u002Fcode> tracks the next available memory address. When Solidity allocates memory (e.g., \u003Ccode>new bytes(100)\u003C\u002Fcode>, \u003Ccode>abi.encode(...)\u003C\u002Fcode>, creating a struct in memory), it reads the pointer, uses that address, and advances the pointer.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F What Solidity does internally when you write:\n\u002F\u002F bytes memory data = new bytes(64);\n\n\u002F\u002F 1. Read free memory pointer: MLOAD(0x40) -&gt; 0x80\n\u002F\u002F 2. Store array length at 0x80: MSTORE(0x80, 64)\n\u002F\u002F 3. Update free memory pointer: MSTORE(0x40, 0x80 + 32 + 64 = 0xE0)\n\u002F\u002F 4. Data lives at 0xA0 through 0xDF\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Memory Expansion Cost\u003C\u002Fh3>\n\u003Cp>Here is where memory gets expensive. The gas cost of memory is not just 3 gas per MLOAD\u002FMSTORE — there is a quadratic expansion cost charged when you access memory beyond the current high-water mark.\u003C\u002Fp>\n\u003Cp>The formula (from the Yellow Paper):\u003C\u002Fp>\n\u003Cpre>\u003Ccode>memory_cost = (memory_size_words^2 \u002F 512) + (3 * memory_size_words)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>where \u003Ccode>memory_size_words = ceil(highest_accessed_byte \u002F 32)\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>For small memory usage (under ~700 bytes), the cost is approximately linear at 3 gas per word. But it grows quadratically:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Memory Used\u003C\u002Fth>\u003Cth>Cost\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>32 bytes (1 word)\u003C\u002Ftd>\u003Ctd>3 gas\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>1 KB (32 words)\u003C\u002Ftd>\u003Ctd>98 gas\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>10 KB (320 words)\u003C\u002Ftd>\u003Ctd>1160 gas\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>100 KB (3200 words)\u003C\u002Ftd>\u003Ctd>29600 gas\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>1 MB (32000 words)\u003C\u002Ftd>\u003Ctd>2,001,000 gas\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>This quadratic growth means that contracts processing large arrays in memory can hit gas limits quickly. It is also why Solidity uses memory sparingly and prefers stack-based computation where possible.\u003C\u002Fp>\n\u003Ch2 id=\"storage-permanent-but-expensive\">Storage: Permanent but Expensive\u003C\u002Fh2>\n\u003Cp>Storage is the EVM’s persistent key-value store. Each contract has its own isolated storage space with 2^256 32-byte slots. Storage persists across transactions — it is the blockchain state.\u003C\u002Fp>\n\u003Ch3>Storage Layout in Solidity\u003C\u002Fh3>\n\u003Cp>Solidity assigns storage slots sequentially to state variables:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">contract StorageLayout {\n    uint256 public x;       \u002F\u002F slot 0\n    uint256 public y;       \u002F\u002F slot 1\n    address public owner;   \u002F\u002F slot 2 (packed: 20 bytes)\n    bool public paused;     \u002F\u002F slot 2 (packed: 1 byte, same slot!)\n    mapping(address =&gt; uint256) public balances;  \u002F\u002F slot 3 (base)\n    \u002F\u002F balances[addr] lives at: keccak256(addr . slot_3)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Mappings and dynamic arrays use \u003Ccode>keccak256\u003C\u002Fcode> to derive their storage slot:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>mapping[key]\u003C\u002Fcode> -&gt; \u003Ccode>keccak256(key || slot_number)\u003C\u002Fcode>\u003C\u002Fli>\n\u003Cli>\u003Ccode>array[index]\u003C\u002Fcode> -&gt; \u003Ccode>keccak256(slot_number) + index\u003C\u002Fcode>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Storage Costs (Post EIP-2929 + EIP-3529)\u003C\u002Fh3>\n\u003Cp>Storage is by far the most expensive data location:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Operation\u003C\u002Fth>\u003Cth>Cold\u003C\u002Fth>\u003Cth>Warm\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>SLOAD\u003C\u002Ftd>\u003Ctd>2100\u003C\u002Ftd>\u003Ctd>100\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>SSTORE (zero to nonzero)\u003C\u002Ftd>\u003Ctd>22100\u003C\u002Ftd>\u003Ctd>20000\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>SSTORE (nonzero to nonzero)\u003C\u002Ftd>\u003Ctd>5000\u003C\u002Ftd>\u003Ctd>2900\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>SSTORE (nonzero to zero)\u003C\u002Ftd>\u003Ctd>5000 + 4800 refund\u003C\u002Ftd>\u003Ctd>2900 + 4800 refund\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>Setting a storage slot from zero to a nonzero value costs \u003Cstrong>20000 gas\u003C\u002Fstrong> because the Ethereum state trie must insert a new leaf node. This is why initializing a new mapping entry (like a token balance for a new holder) is so expensive.\u003C\u002Fp>\n\u003Ch3>Storage Packing\u003C\u002Fh3>\n\u003Cp>Solidity packs multiple small variables into a single 32-byte slot when possible. This is a critical optimization:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F BAD: 3 storage slots = 3 x SLOAD = 6300 gas (cold)\ncontract Unpacked {\n    uint256 a;  \u002F\u002F slot 0 (32 bytes)\n    uint256 b;  \u002F\u002F slot 1 (32 bytes)\n    uint256 c;  \u002F\u002F slot 2 (32 bytes)\n}\n\n\u002F\u002F GOOD: 1 storage slot = 1 x SLOAD = 2100 gas (cold)\ncontract Packed {\n    uint64 a;   \u002F\u002F slot 0, bytes 0-7\n    uint64 b;   \u002F\u002F slot 0, bytes 8-15\n    uint64 c;   \u002F\u002F slot 0, bytes 16-23\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"calldata-read-only-and-cheap\">Calldata: Read-Only and Cheap\u003C\u002Fh2>\n\u003Cp>Calldata is the input data sent with a transaction or a call. It is read-only, and accessing it is cheap (3 gas for CALLDATALOAD). For external function calls, Solidity ABI-encodes the arguments into calldata.\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F transfer(address to, uint256 amount)\n\u002F\u002F calldata: 0xa9059cbb\n\u002F\u002F           0000000000000000000000001234567890abcdef1234567890abcdef12345678\n\u002F\u002F           0000000000000000000000000000000000000000000000000de0b6b3a7640000\n\u002F\u002F           ^-- function selector (4 bytes)\n\u002F\u002F                     ^-- address, padded to 32 bytes\n\u002F\u002F                                                                              ^-- uint256 amount\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Using \u003Ccode>calldata\u003C\u002Fcode> instead of \u003Ccode>memory\u003C\u002Fcode> for function parameters avoids copying data and saves significant gas:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F BAD: copies entire array to memory\nfunction sum(uint256[] memory arr) external returns (uint256) { ... }\n\n\u002F\u002F GOOD: reads directly from calldata\nfunction sum(uint256[] calldata arr) external returns (uint256) { ... }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"transient-storage-eip-1153\">Transient Storage (EIP-1153)\u003C\u002Fh2>\n\u003Cp>EIP-1153 (Cancun upgrade, March 2024) introduced two new opcodes: \u003Cstrong>TSTORE\u003C\u002Fstrong> and \u003Cstrong>TLOAD\u003C\u002Fstrong>. Transient storage is like regular storage but it is automatically cleared at the end of the transaction.\u003C\u002Fp>\n\u003Cp>Key properties:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Same key-value model as storage (2^256 slots of 32 bytes)\u003C\u002Fli>\n\u003Cli>Costs 100 gas for both TLOAD and TSTORE (similar to warm SLOAD\u002FSSTORE)\u003C\u002Fli>\n\u003Cli>Cleared after transaction completes — no permanent state change\u003C\u002Fli>\n\u003Cli>Accessible across call frames within the same transaction\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>This is perfect for:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Reentrancy locks\u003C\u002Fstrong> — Set a flag with TSTORE, check with TLOAD. No 20000 gas SSTORE cost.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Cross-contract communication within a transaction\u003C\u002Fstrong> — Callback patterns, flash loans.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Temporary approvals\u003C\u002Fstrong> — ERC-20 temporary allowances within a single transaction.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cpre>\u003Ccode class=\"language-solidity\">\u002F\u002F Before EIP-1153: reentrancy lock costs ~5000-20000 gas\nmapping(address =&gt; bool) private _locked;\n\n\u002F\u002F After EIP-1153: reentrancy lock costs ~200 gas\nassembly {\n    if tload(0) { revert(0, 0) }  \u002F\u002F check lock\n    tstore(0, 1)                   \u002F\u002F set lock\n}\n\u002F\u002F ... function body ...\nassembly {\n    tstore(0, 0)                   \u002F\u002F clear lock\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"returndata-the-fifth-location\">Returndata: The Fifth Location\u003C\u002Fh2>\n\u003Cp>There is actually a fifth data location: \u003Cstrong>returndata\u003C\u002Fstrong>. After a CALL, STATICCALL, or DELEGATECALL completes, the callee’s return data is available via RETURNDATASIZE and RETURNDATACOPY. This data exists only until the next call opcode overwrites it.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Read return data after an external call\nlet success := call(gas(), target, 0, 0, 0, 0, 0)\nlet size := returndatasize()\nlet ptr := mload(0x40)\nreturndatacopy(ptr, 0, size)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"practical-optimization-choosing-the-right-location\">Practical Optimization: Choosing the Right Location\u003C\u002Fh2>\n\u003Cp>Here is a decision framework for MEV bot developers:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Can it live on the stack?\u003C\u002Fstrong> Use the stack. Zero marginal cost.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Do you need more than 16 values?\u003C\u002Fstrong> Use memory. Allocate from the free memory pointer.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Is it read-only input data?\u003C\u002Fstrong> Use calldata. 3 gas per 32-byte read.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Must it persist across transactions?\u003C\u002Fstrong> Use storage. Budget 20000+ gas for new entries.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Must it persist across call frames but not across transactions?\u003C\u002Fstrong> Use transient storage. 100 gas.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"conclusion\">Conclusion\u003C\u002Fh2>\n\u003Cp>The EVM’s memory model is deceptively simple — four locations with clear tradeoffs. But the cost differences are enormous: a stack ADD costs 3 gas while an SSTORE costs 20000. Understanding these costs at a visceral level is what separates gas-efficient contracts from gas-burning ones. In the next article, we will quantify gas costs precisely and explore optimization patterns that can save thousands of gas per call.\u003C\u002Fp>\n","en","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:22.660097Z","Deep dive into the EVM memory model: stack, memory, storage, calldata, transient storage, and the memory expansion cost formula.","EVM memory model",null,"index, follow",[21,26,30],{"id":22,"name":23,"slug":24,"created_at":25},"c0000000-0000-0000-0000-000000000016","EVM","evm","2026-03-28T10:44:21.513630Z",{"id":27,"name":28,"slug":29,"created_at":25},"c0000000-0000-0000-0000-000000000020","Gas Optimization","gas-optimization",{"id":31,"name":32,"slug":33,"created_at":25},"c0000000-0000-0000-0000-000000000014","Solidity","solidity","Blockchain",[36,42,48],{"id":37,"title":38,"slug":39,"excerpt":40,"locale":12,"category_name":34,"published_at":41},"de000000-0000-0000-0000-000000000003","The Ethereum Interoperability Layer: How 55+ L2s Become One Chain","ethereum-interoperability-layer-how-55-l2s-become-one-chain","Ethereum has 55+ Layer 2 rollups, fragmenting liquidity and user experience. The Ethereum Interoperability Layer — combining cross-rollup messaging, shared sequencers, and based rollups — aims to unify them into a single composable network.","2026-03-28T10:44:35.632478Z",{"id":43,"title":44,"slug":45,"excerpt":46,"locale":12,"category_name":34,"published_at":47},"de000000-0000-0000-0000-000000000002","ZK Proofs Beyond Rollups: Verifiable AI Inference on Ethereum","zk-proofs-beyond-rollups-verifiable-ai-inference-ethereum","Zero-knowledge proofs are no longer just a scaling tool. In 2026, zkML enables verifiable AI inference on-chain, ZK coprocessors move heavy computation off-chain with on-chain verification, and new proving systems like SP1 and Jolt make it practical.","2026-03-28T10:44:35.618408Z",{"id":49,"title":50,"slug":51,"excerpt":52,"locale":12,"category_name":34,"published_at":53},"dd000000-0000-0000-0000-000000000003","EIP-7702 in Practice: Building Smart Account Flows After Pectra","eip-7702-in-practice-building-smart-account-flows-after-pectra","EIP-7702 lets any Ethereum EOA temporarily act as a smart contract within a single transaction. Here is how to implement batch transactions, gas sponsorship, and social recovery using the new account abstraction primitive.","2026-03-28T10:44:35.031290Z",{"id":13,"name":55,"slug":56,"bio":57,"photo_url":18,"linkedin":18,"role":58,"created_at":59,"updated_at":59},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]