[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-6-yul-memory-management-mstore-mload":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-000000000106","a0000000-0000-0000-0000-000000000002","Deep EVM #6: Yul Memory Management — mstore, mload, and Free Memory Pointer","deep-evm-6-yul-memory-management-mstore-mload","Master EVM memory management in Yul: the free memory pointer, manual ABI encoding, building external call data, and memory-efficient patterns for gas-critical contracts.","## Memory Management Without a Garbage Collector\n\nIn Yul, you are the memory manager. There is no allocator, no garbage collector, no bounds checking. Memory is a flat byte array that you read from and write to using MLOAD and MSTORE. If you write to the wrong offset, you corrupt your own data. If you read from an uninitialized offset, you get garbage.\n\nThis level of control is exactly what makes Yul powerful for gas optimization — and exactly what makes it dangerous.\n\n## MSTORE, MLOAD, and MSTORE8\n\nThree opcodes handle memory access:\n\n```yul\n\u002F\u002F MSTORE(offset, value) — write 32 bytes at offset\nmstore(0x00, 0xdeadbeef)  \u002F\u002F Stores 0x00...00deadbeef at offset 0\n\n\u002F\u002F MLOAD(offset) — read 32 bytes from offset\nlet value := mload(0x00)  \u002F\u002F value = 0x00...00deadbeef\n\n\u002F\u002F MSTORE8(offset, value) — write 1 byte at offset\nmstore8(0x00, 0xff)  \u002F\u002F Stores 0xff at byte 0\n```\n\nCritical detail: MSTORE writes big-endian. The value `0xdeadbeef` stored at offset 0x00 occupies bytes 28-31 (the least significant 4 bytes of the 32-byte word). The first 28 bytes are zeros.\n\n```\nMemory after mstore(0x00, 0xdeadbeef):\nOffset: 00 01 02 03 ... 1b 1c 1d 1e 1f\nValue:  00 00 00 00 ... 00 de ad be ef\n```\n\n## The Free Memory Pointer (0x40)\n\nSolidity initializes the free memory pointer at offset 0x40 to the value 0x80. This means usable memory starts at offset 0x80, with 0x00-0x7f reserved:\n\n```\nMemory layout (Solidity convention):\n0x00-0x1f: Scratch space (temporary, used for hashing)\n0x20-0x3f: Scratch space (temporary)\n0x40-0x5f: Free memory pointer (initially 0x80)\n0x60-0x7f: Zero slot (always zero, used for default values)\n0x80+:     Free memory (your allocations start here)\n```\n\nWhen you mix Yul with Solidity, you must respect this convention. If Solidity code runs after your Yul block, it expects the free memory pointer to accurately track the next free byte.\n\n### Allocating Memory in Yul\n\n```yul\nfunction allocate(size) -> ptr {\n    ptr := mload(0x40)            \u002F\u002F Read current free memory pointer\n    mstore(0x40, add(ptr, size))  \u002F\u002F Advance it by 'size' bytes\n}\n\n\u002F\u002F Allocate 64 bytes\nlet buffer := allocate(64)\nmstore(buffer, 0x1234)  \u002F\u002F Write to allocated memory\n```\n\n### When to Ignore the Free Memory Pointer\n\nIn pure Yul contracts (no Solidity), you can ignore the free memory pointer entirely and manage memory manually. Many MEV bots do this:\n\n```yul\n\u002F\u002F Pure Yul contract — use memory however you want\n\u002F\u002F No Solidity, no free memory pointer convention\nmstore(0x00, calldataload(0))   \u002F\u002F Use 0x00 directly\nmstore(0x20, calldataload(32))  \u002F\u002F Use 0x20 directly\n```\n\nThis saves gas: no MLOAD(0x40) and no pointer update. But you must track your own memory layout.\n\n## Building ABI-Encoded Calldata in Yul\n\nOne of the most common Yul tasks is building calldata for external calls. Let us construct a `transfer(address,uint256)` call:\n\n```yul\n\u002F\u002F transfer(address to, uint256 amount)\n\u002F\u002F Selector: 0xa9059cbb\n\nfunction encodeTransfer(to, amount) -> ptr, size {\n    ptr := mload(0x40)  \u002F\u002F allocate from free memory\n\n    \u002F\u002F Byte 0-3: function selector\n    mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)\n\n    \u002F\u002F Byte 4-35: address (left-padded to 32 bytes)\n    mstore(add(ptr, 0x04), to)\n\n    \u002F\u002F Byte 36-67: uint256 amount\n    mstore(add(ptr, 0x24), amount)\n\n    size := 0x44  \u002F\u002F 4 + 32 + 32 = 68 bytes\n    mstore(0x40, add(ptr, size))  \u002F\u002F update free memory pointer\n}\n```\n\nWait — there is a subtle bug above. When we `mstore(ptr, selector)`, we write 32 bytes starting at `ptr`. Then `mstore(add(ptr, 0x04), to)` overwrites bytes 4-35, which partially overlaps the selector write. This is actually fine because the second write overwrites bytes 4-31 (which were zeros from the selector padding) with the address value.\n\nBut a cleaner approach uses the scratch space:\n\n```yul\nfunction doTransfer(token, to, amount) {\n    \u002F\u002F Use scratch space at 0x00 — we know it is safe to overwrite\n    mstore(0x00, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, to)\n    mstore(0x24, amount)\n\n    let success := call(\n        gas(),    \u002F\u002F forward all gas\n        token,    \u002F\u002F target contract\n        0,        \u002F\u002F no ETH value\n        0x00,     \u002F\u002F calldata starts at memory offset 0\n        0x44,     \u002F\u002F calldata length: 68 bytes\n        0x00,     \u002F\u002F return data offset (overwrite scratch space)\n        0x20      \u002F\u002F return data length: 32 bytes\n    )\n\n    \u002F\u002F Check success and return value\n    if iszero(and(success, or(\n        iszero(returndatasize()),                    \u002F\u002F no return data (USDT)\n        and(gt(returndatasize(), 31), mload(0x00))   \u002F\u002F returned true\n    ))) {\n        revert(0, 0)\n    }\n}\n```\n\nThis pattern — store calldata at 0x00, call, check return — is the standard for gas-efficient token transfers in MEV bots.\n\n## Handling Dynamic Types: bytes and string\n\nDynamic types in ABI encoding use an offset-pointer structure:\n\n```\n\u002F\u002F abi.encode(uint256 x, bytes memory data, uint256 y)\n\u002F\u002F Layout in memory:\n\u002F\u002F 0x00: x (value directly)\n\u002F\u002F 0x20: offset to 'data' (points to 0x60)\n\u002F\u002F 0x40: y (value directly)\n\u002F\u002F 0x60: length of 'data'\n\u002F\u002F 0x80: actual bytes of 'data' (padded to 32-byte boundary)\n```\n\nEncoding this in Yul:\n\n```yul\nfunction encodeMixed(x, dataPtr, dataLen, y) -> outPtr, outSize {\n    outPtr := mload(0x40)\n\n    \u002F\u002F Static part\n    mstore(outPtr, x)                           \u002F\u002F word 0: x\n    mstore(add(outPtr, 0x20), 0x60)            \u002F\u002F word 1: offset to data (3 * 32 = 0x60)\n    mstore(add(outPtr, 0x40), y)               \u002F\u002F word 2: y\n\n    \u002F\u002F Dynamic part\n    mstore(add(outPtr, 0x60), dataLen)         \u002F\u002F word 3: data length\n\n    \u002F\u002F Copy data bytes\n    let words := div(add(dataLen, 31), 32)     \u002F\u002F round up to 32-byte words\n    for { let i := 0 } lt(i, words) { i := add(i, 1) } {\n        mstore(\n            add(add(outPtr, 0x80), mul(i, 0x20)),\n            mload(add(dataPtr, mul(i, 0x20)))\n        )\n    }\n\n    outSize := add(0x80, mul(words, 0x20))\n    mstore(0x40, add(outPtr, outSize))\n}\n```\n\n## Memory-Efficient Hashing\n\nKeccak256 hashing is a frequent operation (storage slot computation, event topics, signatures). In Yul, you control exactly what gets hashed:\n\n```yul\n\u002F\u002F Hash a mapping key: keccak256(abi.encode(key, slot))\nfunction getMappingSlot(key, baseSlot) -> slot {\n    mstore(0x00, key)\n    mstore(0x20, baseSlot)\n    slot := keccak256(0x00, 0x40)  \u002F\u002F hash 64 bytes starting at 0x00\n}\n\n\u002F\u002F Hash a nested mapping: mapping[key1][key2]\nfunction getNestedMappingSlot(key1, key2, baseSlot) -> slot {\n    \u002F\u002F First level: keccak256(key1 . baseSlot)\n    mstore(0x00, key1)\n    mstore(0x20, baseSlot)\n    let intermediate := keccak256(0x00, 0x40)\n\n    \u002F\u002F Second level: keccak256(key2 . intermediate)\n    mstore(0x00, key2)\n    mstore(0x20, intermediate)\n    slot := keccak256(0x00, 0x40)\n}\n```\n\nUsing scratch space (0x00-0x3f) for hashing is a common pattern because it avoids advancing the free memory pointer.\n\n## Copying Data Between Memory Regions\n\nSolidity's `abi.encode` and `bytes` concatenation generate memory copies. In Yul, you can do this manually:\n\n```yul\n\u002F\u002F Copy 'length' bytes from 'src' to 'dst'\nfunction memcpy(dst, src, length) {\n    \u002F\u002F Copy 32-byte chunks\n    for { let i := 0 } lt(i, length) { i := add(i, 0x20) } {\n        mstore(add(dst, i), mload(add(src, i)))\n    }\n}\n```\n\nFor calldata-to-memory copies, use CALLDATACOPY which is cheaper than a loop:\n\n```yul\n\u002F\u002F Copy calldata to memory (single opcode, cheaper than loop)\ncalldatacopy(destOffset, calldataOffset, length)\n```\n\nSimilarly, CODECOPY and RETURNDATACOPY are single opcodes that are more gas-efficient than loops.\n\n## Memory Expansion Cost Revisited\n\nRecall from the previous article that memory has a quadratic expansion cost. In Yul, you must be aware of the high-water mark:\n\n```yul\n\u002F\u002F This is CHEAP (memory already expanded to 0x80 by Solidity init)\nmstore(0x00, 42)\n\n\u002F\u002F This is EXPENSIVE (first access beyond current memory size)\nmstore(0x10000, 42)  \u002F\u002F Expands memory to 64KB, costs ~1600 gas just for expansion\n\n\u002F\u002F Reading also triggers expansion!\nlet x := mload(0x20000)  \u002F\u002F Expands memory to 128KB\n```\n\nAlways use the lowest memory offsets possible. In pure Yul contracts, start at 0x00 and allocate sequentially.\n\n## Pattern: Returndata Forwarding\n\nAfter an external call, you often need to forward the return data. Here is the gas-optimal pattern:\n\n```yul\n\u002F\u002F Call target and forward return data (or revert data)\nlet success := call(gas(), target, value, inOffset, inSize, 0, 0)\nlet size := returndatasize()\nreturndatacopy(0x00, 0x00, size)\n\nswitch success\ncase 0 { revert(0x00, size) }\ndefault { return(0x00, size) }\n```\n\nThis pattern uses zero for retOffset and retSize in the CALL (saving gas by not pre-allocating return buffer), then copies return data afterward. This is the standard pattern in proxy contracts.\n\n## Pattern: Compact Error Handling\n\n```yul\n\u002F\u002F Custom error: InsufficientBalance(uint256 available, uint256 required)\n\u002F\u002F Selector: keccak256(\"InsufficientBalance(uint256,uint256)\") >> 224\nfunction revertInsufficientBalance(available, required) {\n    mstore(0x00, 0x0a087903)  \u002F\u002F error selector (4 bytes, left-aligned would need shl)\n    mstore(0x04, available)\n    mstore(0x24, required)\n    revert(0x00, 0x44)  \u002F\u002F revert with 68 bytes\n}\n```\n\nThis costs significantly less gas than Solidity's `require(balance >= amount, \"Insufficient balance\")` because there is no string storage or ABI encoding of the string.\n\n## Real-World Example: Extracting Uniswap V2 Reserves\n\n```yul\nfunction getReserves(pair) -> reserve0, reserve1 {\n    \u002F\u002F getReserves() selector: 0x0902f1ac\n    mstore(0x00, 0x0902f1ac00000000000000000000000000000000000000000000000000000000)\n\n    let success := staticcall(gas(), pair, 0x00, 0x04, 0x00, 0x60)\n    if iszero(success) { revert(0, 0) }\n\n    reserve0 := mload(0x00)\n    reserve1 := mload(0x20)\n    \u002F\u002F Third return value (blockTimestampLast) at 0x40 — ignored\n}\n```\n\nThis is how MEV bots read pool reserves: a single staticcall with minimal memory usage, no ABI decoding overhead, and the result is immediately available as stack variables.\n\n## Conclusion\n\nMemory management in Yul is manual, precise, and powerful. The patterns in this article — scratch space usage, calldata construction, hash computation, returndata forwarding — are the building blocks of every gas-optimized smart contract. Master these, and you can build anything the EVM can execute.\n\nIn the next article, we tackle control flow: gas-efficient loops, conditionals, and the specific patterns that minimize gas in iteration-heavy code.","\u003Ch2 id=\"memory-management-without-a-garbage-collector\">Memory Management Without a Garbage Collector\u003C\u002Fh2>\n\u003Cp>In Yul, you are the memory manager. There is no allocator, no garbage collector, no bounds checking. Memory is a flat byte array that you read from and write to using MLOAD and MSTORE. If you write to the wrong offset, you corrupt your own data. If you read from an uninitialized offset, you get garbage.\u003C\u002Fp>\n\u003Cp>This level of control is exactly what makes Yul powerful for gas optimization — and exactly what makes it dangerous.\u003C\u002Fp>\n\u003Ch2 id=\"mstore-mload-and-mstore8\">MSTORE, MLOAD, and MSTORE8\u003C\u002Fh2>\n\u003Cp>Three opcodes handle memory access:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F MSTORE(offset, value) — write 32 bytes at offset\nmstore(0x00, 0xdeadbeef)  \u002F\u002F Stores 0x00...00deadbeef at offset 0\n\n\u002F\u002F MLOAD(offset) — read 32 bytes from offset\nlet value := mload(0x00)  \u002F\u002F value = 0x00...00deadbeef\n\n\u002F\u002F MSTORE8(offset, value) — write 1 byte at offset\nmstore8(0x00, 0xff)  \u002F\u002F Stores 0xff at byte 0\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Critical detail: MSTORE writes big-endian. The value \u003Ccode>0xdeadbeef\u003C\u002Fcode> stored at offset 0x00 occupies bytes 28-31 (the least significant 4 bytes of the 32-byte word). The first 28 bytes are zeros.\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Memory after mstore(0x00, 0xdeadbeef):\nOffset: 00 01 02 03 ... 1b 1c 1d 1e 1f\nValue:  00 00 00 00 ... 00 de ad be ef\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"the-free-memory-pointer-0x40\">The Free Memory Pointer (0x40)\u003C\u002Fh2>\n\u003Cp>Solidity initializes the free memory pointer at offset 0x40 to the value 0x80. This means usable memory starts at offset 0x80, with 0x00-0x7f reserved:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>Memory layout (Solidity convention):\n0x00-0x1f: Scratch space (temporary, used for hashing)\n0x20-0x3f: Scratch space (temporary)\n0x40-0x5f: Free memory pointer (initially 0x80)\n0x60-0x7f: Zero slot (always zero, used for default values)\n0x80+:     Free memory (your allocations start here)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>When you mix Yul with Solidity, you must respect this convention. If Solidity code runs after your Yul block, it expects the free memory pointer to accurately track the next free byte.\u003C\u002Fp>\n\u003Ch3>Allocating Memory in Yul\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-yul\">function allocate(size) -&gt; ptr {\n    ptr := mload(0x40)            \u002F\u002F Read current free memory pointer\n    mstore(0x40, add(ptr, size))  \u002F\u002F Advance it by 'size' bytes\n}\n\n\u002F\u002F Allocate 64 bytes\nlet buffer := allocate(64)\nmstore(buffer, 0x1234)  \u002F\u002F Write to allocated memory\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>When to Ignore the Free Memory Pointer\u003C\u002Fh3>\n\u003Cp>In pure Yul contracts (no Solidity), you can ignore the free memory pointer entirely and manage memory manually. Many MEV bots do this:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Pure Yul contract — use memory however you want\n\u002F\u002F No Solidity, no free memory pointer convention\nmstore(0x00, calldataload(0))   \u002F\u002F Use 0x00 directly\nmstore(0x20, calldataload(32))  \u002F\u002F Use 0x20 directly\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This saves gas: no MLOAD(0x40) and no pointer update. But you must track your own memory layout.\u003C\u002Fp>\n\u003Ch2 id=\"building-abi-encoded-calldata-in-yul\">Building ABI-Encoded Calldata in Yul\u003C\u002Fh2>\n\u003Cp>One of the most common Yul tasks is building calldata for external calls. Let us construct a \u003Ccode>transfer(address,uint256)\u003C\u002Fcode> call:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F transfer(address to, uint256 amount)\n\u002F\u002F Selector: 0xa9059cbb\n\nfunction encodeTransfer(to, amount) -&gt; ptr, size {\n    ptr := mload(0x40)  \u002F\u002F allocate from free memory\n\n    \u002F\u002F Byte 0-3: function selector\n    mstore(ptr, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)\n\n    \u002F\u002F Byte 4-35: address (left-padded to 32 bytes)\n    mstore(add(ptr, 0x04), to)\n\n    \u002F\u002F Byte 36-67: uint256 amount\n    mstore(add(ptr, 0x24), amount)\n\n    size := 0x44  \u002F\u002F 4 + 32 + 32 = 68 bytes\n    mstore(0x40, add(ptr, size))  \u002F\u002F update free memory pointer\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Wait — there is a subtle bug above. When we \u003Ccode>mstore(ptr, selector)\u003C\u002Fcode>, we write 32 bytes starting at \u003Ccode>ptr\u003C\u002Fcode>. Then \u003Ccode>mstore(add(ptr, 0x04), to)\u003C\u002Fcode> overwrites bytes 4-35, which partially overlaps the selector write. This is actually fine because the second write overwrites bytes 4-31 (which were zeros from the selector padding) with the address value.\u003C\u002Fp>\n\u003Cp>But a cleaner approach uses the scratch space:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">function doTransfer(token, to, amount) {\n    \u002F\u002F Use scratch space at 0x00 — we know it is safe to overwrite\n    mstore(0x00, 0xa9059cbb00000000000000000000000000000000000000000000000000000000)\n    mstore(0x04, to)\n    mstore(0x24, amount)\n\n    let success := call(\n        gas(),    \u002F\u002F forward all gas\n        token,    \u002F\u002F target contract\n        0,        \u002F\u002F no ETH value\n        0x00,     \u002F\u002F calldata starts at memory offset 0\n        0x44,     \u002F\u002F calldata length: 68 bytes\n        0x00,     \u002F\u002F return data offset (overwrite scratch space)\n        0x20      \u002F\u002F return data length: 32 bytes\n    )\n\n    \u002F\u002F Check success and return value\n    if iszero(and(success, or(\n        iszero(returndatasize()),                    \u002F\u002F no return data (USDT)\n        and(gt(returndatasize(), 31), mload(0x00))   \u002F\u002F returned true\n    ))) {\n        revert(0, 0)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This pattern — store calldata at 0x00, call, check return — is the standard for gas-efficient token transfers in MEV bots.\u003C\u002Fp>\n\u003Ch2 id=\"handling-dynamic-types-bytes-and-string\">Handling Dynamic Types: bytes and string\u003C\u002Fh2>\n\u003Cp>Dynamic types in ABI encoding use an offset-pointer structure:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F\u002F abi.encode(uint256 x, bytes memory data, uint256 y)\n\u002F\u002F Layout in memory:\n\u002F\u002F 0x00: x (value directly)\n\u002F\u002F 0x20: offset to 'data' (points to 0x60)\n\u002F\u002F 0x40: y (value directly)\n\u002F\u002F 0x60: length of 'data'\n\u002F\u002F 0x80: actual bytes of 'data' (padded to 32-byte boundary)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Encoding this in Yul:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">function encodeMixed(x, dataPtr, dataLen, y) -&gt; outPtr, outSize {\n    outPtr := mload(0x40)\n\n    \u002F\u002F Static part\n    mstore(outPtr, x)                           \u002F\u002F word 0: x\n    mstore(add(outPtr, 0x20), 0x60)            \u002F\u002F word 1: offset to data (3 * 32 = 0x60)\n    mstore(add(outPtr, 0x40), y)               \u002F\u002F word 2: y\n\n    \u002F\u002F Dynamic part\n    mstore(add(outPtr, 0x60), dataLen)         \u002F\u002F word 3: data length\n\n    \u002F\u002F Copy data bytes\n    let words := div(add(dataLen, 31), 32)     \u002F\u002F round up to 32-byte words\n    for { let i := 0 } lt(i, words) { i := add(i, 1) } {\n        mstore(\n            add(add(outPtr, 0x80), mul(i, 0x20)),\n            mload(add(dataPtr, mul(i, 0x20)))\n        )\n    }\n\n    outSize := add(0x80, mul(words, 0x20))\n    mstore(0x40, add(outPtr, outSize))\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"memory-efficient-hashing\">Memory-Efficient Hashing\u003C\u002Fh2>\n\u003Cp>Keccak256 hashing is a frequent operation (storage slot computation, event topics, signatures). In Yul, you control exactly what gets hashed:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Hash a mapping key: keccak256(abi.encode(key, slot))\nfunction getMappingSlot(key, baseSlot) -&gt; slot {\n    mstore(0x00, key)\n    mstore(0x20, baseSlot)\n    slot := keccak256(0x00, 0x40)  \u002F\u002F hash 64 bytes starting at 0x00\n}\n\n\u002F\u002F Hash a nested mapping: mapping[key1][key2]\nfunction getNestedMappingSlot(key1, key2, baseSlot) -&gt; slot {\n    \u002F\u002F First level: keccak256(key1 . baseSlot)\n    mstore(0x00, key1)\n    mstore(0x20, baseSlot)\n    let intermediate := keccak256(0x00, 0x40)\n\n    \u002F\u002F Second level: keccak256(key2 . intermediate)\n    mstore(0x00, key2)\n    mstore(0x20, intermediate)\n    slot := keccak256(0x00, 0x40)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Using scratch space (0x00-0x3f) for hashing is a common pattern because it avoids advancing the free memory pointer.\u003C\u002Fp>\n\u003Ch2 id=\"copying-data-between-memory-regions\">Copying Data Between Memory Regions\u003C\u002Fh2>\n\u003Cp>Solidity’s \u003Ccode>abi.encode\u003C\u002Fcode> and \u003Ccode>bytes\u003C\u002Fcode> concatenation generate memory copies. In Yul, you can do this manually:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Copy 'length' bytes from 'src' to 'dst'\nfunction memcpy(dst, src, length) {\n    \u002F\u002F Copy 32-byte chunks\n    for { let i := 0 } lt(i, length) { i := add(i, 0x20) } {\n        mstore(add(dst, i), mload(add(src, i)))\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>For calldata-to-memory copies, use CALLDATACOPY which is cheaper than a loop:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Copy calldata to memory (single opcode, cheaper than loop)\ncalldatacopy(destOffset, calldataOffset, length)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Similarly, CODECOPY and RETURNDATACOPY are single opcodes that are more gas-efficient than loops.\u003C\u002Fp>\n\u003Ch2 id=\"memory-expansion-cost-revisited\">Memory Expansion Cost Revisited\u003C\u002Fh2>\n\u003Cp>Recall from the previous article that memory has a quadratic expansion cost. In Yul, you must be aware of the high-water mark:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F This is CHEAP (memory already expanded to 0x80 by Solidity init)\nmstore(0x00, 42)\n\n\u002F\u002F This is EXPENSIVE (first access beyond current memory size)\nmstore(0x10000, 42)  \u002F\u002F Expands memory to 64KB, costs ~1600 gas just for expansion\n\n\u002F\u002F Reading also triggers expansion!\nlet x := mload(0x20000)  \u002F\u002F Expands memory to 128KB\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Always use the lowest memory offsets possible. In pure Yul contracts, start at 0x00 and allocate sequentially.\u003C\u002Fp>\n\u003Ch2 id=\"pattern-returndata-forwarding\">Pattern: Returndata Forwarding\u003C\u002Fh2>\n\u003Cp>After an external call, you often need to forward the return data. Here is the gas-optimal pattern:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Call target and forward return data (or revert data)\nlet success := call(gas(), target, value, inOffset, inSize, 0, 0)\nlet size := returndatasize()\nreturndatacopy(0x00, 0x00, size)\n\nswitch success\ncase 0 { revert(0x00, size) }\ndefault { return(0x00, size) }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This pattern uses zero for retOffset and retSize in the CALL (saving gas by not pre-allocating return buffer), then copies return data afterward. This is the standard pattern in proxy contracts.\u003C\u002Fp>\n\u003Ch2 id=\"pattern-compact-error-handling\">Pattern: Compact Error Handling\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F Custom error: InsufficientBalance(uint256 available, uint256 required)\n\u002F\u002F Selector: keccak256(\"InsufficientBalance(uint256,uint256)\") &gt;&gt; 224\nfunction revertInsufficientBalance(available, required) {\n    mstore(0x00, 0x0a087903)  \u002F\u002F error selector (4 bytes, left-aligned would need shl)\n    mstore(0x04, available)\n    mstore(0x24, required)\n    revert(0x00, 0x44)  \u002F\u002F revert with 68 bytes\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This costs significantly less gas than Solidity’s \u003Ccode>require(balance &gt;= amount, \"Insufficient balance\")\u003C\u002Fcode> because there is no string storage or ABI encoding of the string.\u003C\u002Fp>\n\u003Ch2 id=\"real-world-example-extracting-uniswap-v2-reserves\">Real-World Example: Extracting Uniswap V2 Reserves\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-yul\">function getReserves(pair) -&gt; reserve0, reserve1 {\n    \u002F\u002F getReserves() selector: 0x0902f1ac\n    mstore(0x00, 0x0902f1ac00000000000000000000000000000000000000000000000000000000)\n\n    let success := staticcall(gas(), pair, 0x00, 0x04, 0x00, 0x60)\n    if iszero(success) { revert(0, 0) }\n\n    reserve0 := mload(0x00)\n    reserve1 := mload(0x20)\n    \u002F\u002F Third return value (blockTimestampLast) at 0x40 — ignored\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This is how MEV bots read pool reserves: a single staticcall with minimal memory usage, no ABI decoding overhead, and the result is immediately available as stack variables.\u003C\u002Fp>\n\u003Ch2 id=\"conclusion\">Conclusion\u003C\u002Fh2>\n\u003Cp>Memory management in Yul is manual, precise, and powerful. The patterns in this article — scratch space usage, calldata construction, hash computation, returndata forwarding — are the building blocks of every gas-optimized smart contract. Master these, and you can build anything the EVM can execute.\u003C\u002Fp>\n\u003Cp>In the next article, we tackle control flow: gas-efficient loops, conditionals, and the specific patterns that minimize gas in iteration-heavy code.\u003C\u002Fp>\n","en","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:22.688288Z","Master Yul memory management: free memory pointer, manual ABI encoding, calldata construction, and memory-efficient patterns for gas-critical EVM contracts.","Yul memory management",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-000000000018","Yul","yul","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"]