[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-5-introduction-to-yul-assembly":3},{"article":4,"author":58},{"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":38,"related_articles":39},"d0000000-0000-0000-0000-000000000105","a0000000-0000-0000-0000-000000000002","Deep EVM #5: Introduction to Yul — Solidity's Secret Assembly Language","deep-evm-5-introduction-to-yul-assembly","A practical introduction to Yul: syntax, inline assembly in Solidity, when to use it, and hands-on examples for reading storage, emitting events, and bypassing Solidity's overhead.","## What Is Yul and Why Does It Exist\n\nYul is an intermediate language designed by the Solidity team that compiles to EVM bytecode (and, in theory, to eWASM). It sits between Solidity and raw opcodes: you get direct access to every EVM opcode through a readable, structured syntax with variables, functions, if\u002Fswitch\u002Ffor constructs, and scoped blocks.\n\nYul exists because Solidity's compiler generates conservative bytecode. Overflow checks, ABI encoding, memory management — all useful for application contracts but unacceptable overhead for gas-critical code like MEV bots, DEX routers, or on-chain math libraries.\n\nWhen you write `assembly { ... }` inside Solidity, you are writing Yul.\n\n## Yul Syntax Fundamentals\n\nYul's syntax is minimal. There are no types — everything is a 256-bit word. There are no arrays, no structs, no inheritance. You have:\n\n### Variables\n```yul\nlet x := 42\nlet y := add(x, 1)    \u002F\u002F y = 43\nlet z := mul(x, y)    \u002F\u002F z = 42 * 43 = 1806\n```\n\nVariables are scoped to the enclosing block `{ }`. They live on the stack. The Yul compiler manages stack layout for you (unlike Huff, where you manage the stack manually).\n\n### Functions\n```yul\nfunction safeAdd(a, b) -> result {\n    result := add(a, b)\n    if lt(result, a) {\n        revert(0, 0)  \u002F\u002F overflow\n    }\n}\n\nlet sum := safeAdd(100, 200)\n```\n\nFunctions can have multiple return values:\n```yul\nfunction divmod(a, b) -> quotient, remainder {\n    quotient := div(a, b)\n    remainder := mod(a, b)\n}\n\nlet q, r := divmod(17, 5)  \u002F\u002F q = 3, r = 2\n```\n\n### Control Flow\n\n**If** (no else in Yul!):\n```yul\nif iszero(calldatasize()) {\n    revert(0, 0)\n}\n```\n\n**Switch** (the else equivalent):\n```yul\nswitch selector\ncase 0xa9059cbb {  \u002F\u002F transfer(address,uint256)\n    \u002F\u002F handle transfer\n}\ncase 0x70a08231 {  \u002F\u002F balanceOf(address)\n    \u002F\u002F handle balanceOf\n}\ndefault {\n    revert(0, 0)\n}\n```\n\n**For** loops:\n```yul\nfor { let i := 0 } lt(i, 10) { i := add(i, 1) } {\n    \u002F\u002F loop body\n}\n```\n\n## Inline Assembly in Solidity\n\nThe most common way to use Yul is inside Solidity's `assembly { }` blocks. Solidity variables are accessible within the block:\n\n```solidity\nfunction getBalance(address account) external view returns (uint256 bal) {\n    bytes32 slot;\n    assembly {\n        \u002F\u002F balances mapping is at storage slot 0\n        \u002F\u002F balances[account] = keccak256(account . slot_0)\n        mstore(0x00, account)\n        mstore(0x20, 0)  \u002F\u002F slot number\n        slot := keccak256(0x00, 0x40)\n        bal := sload(slot)\n    }\n}\n```\n\nThis bypasses Solidity's mapping accessor, saving gas from ABI encoding and bounds checks.\n\n### Reading and Writing Storage Directly\n\n```solidity\ncontract DirectStorage {\n    \u002F\u002F slot 0: owner (address, 20 bytes)\n    \u002F\u002F slot 1: totalSupply (uint256)\n    \u002F\u002F slot 2: balances mapping base\n\n    function getOwner() external view returns (address o) {\n        assembly {\n            o := sload(0)  \u002F\u002F Read slot 0 directly\n        }\n    }\n\n    function unsafeSetOwner(address newOwner) external {\n        assembly {\n            \u002F\u002F No access control! For demonstration only.\n            sstore(0, newOwner)  \u002F\u002F Write slot 0 directly\n        }\n    }\n}\n```\n\n### Emitting Events in Yul\n\nEvents in Yul are emitted using LOG opcodes (LOG0 through LOG4, where the number is the count of indexed topics):\n\n```solidity\nevent Transfer(address indexed from, address indexed to, uint256 value);\n\nfunction emitTransfer(address from, address to, uint256 value) internal {\n    assembly {\n        \u002F\u002F Transfer(address,address,uint256) topic:\n        let sig := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\n\n        \u002F\u002F Store non-indexed data in memory\n        mstore(0x00, value)\n\n        \u002F\u002F LOG3: 3 topics (event sig + 2 indexed params) + data\n        log3(\n            0x00,    \u002F\u002F memory offset of data\n            0x20,    \u002F\u002F data length (32 bytes)\n            sig,     \u002F\u002F topic 0: event signature\n            from,    \u002F\u002F topic 1: indexed from\n            to       \u002F\u002F topic 2: indexed to\n        )\n    }\n}\n```\n\nThis saves gas compared to Solidity's `emit Transfer(from, to, value)` because we skip ABI encoding overhead.\n\n## When to Use Yul (and When Not To)\n\n### Use Yul When:\n\n1. **Gas is critical** — MEV bots, DEX aggregator routers, high-frequency on-chain operations\n2. **You need bitwise operations** — Packing\u002Funpacking data, custom encoding\n3. **Memory layout matters** — Building calldata for external calls, custom ABI encoding\n4. **Solidity generates suboptimal code** — Certain patterns (like copying between memory regions) compile poorly\n5. **You need transient storage** — TLOAD\u002FTSTORE before Solidity adds native support\n6. **Proxy contracts** — The minimal proxy (EIP-1167) is 45 bytes of hand-optimized Yul\n\n### Do Not Use Yul When:\n\n1. **Readability matters more than gas** — Most application contracts\n2. **You are not confident in your EVM knowledge** — Yul has no safety nets\n3. **The gas savings are negligible** — If saving 200 gas on a 500,000 gas function, it is not worth the audit risk\n4. **You need complex data structures** — Yul has no arrays, structs, or mappings at the language level\n\n## Practical Example: Efficient Multicall\n\nHere is a real-world example — a gas-efficient multicall function that executes multiple calls in a single transaction:\n\n```solidity\nfunction multicall(bytes[] calldata calls) external returns (bytes[] memory results) {\n    results = new bytes[](calls.length);\n    for (uint256 i = 0; i \u003C calls.length;) {\n        (bool success, bytes memory result) = address(this).delegatecall(calls[i]);\n        require(success);\n        results[i] = result;\n        unchecked { ++i; }\n    }\n}\n\n\u002F\u002F Yul-optimized version:\nfunction multicallOptimized(bytes[] calldata calls) external {\n    assembly {\n        let len := calls.length\n        let dataOffset := calls.offset\n\n        for { let i := 0 } lt(i, len) { i := add(i, 1) } {\n            \u002F\u002F Get the offset and length of calls[i]\n            let callOffset := add(dataOffset, calldataload(add(dataOffset, mul(i, 0x20))))\n            let callLen := calldataload(callOffset)\n            let callData := add(callOffset, 0x20)\n\n            \u002F\u002F Copy calldata to memory\n            calldatacopy(0x00, callData, callLen)\n\n            \u002F\u002F Execute delegatecall\n            let success := delegatecall(gas(), address(), 0x00, callLen, 0x00, 0x00)\n            if iszero(success) {\n                returndatacopy(0x00, 0x00, returndatasize())\n                revert(0x00, returndatasize())\n            }\n        }\n    }\n}\n```\n\n## Practical Example: Reading Uniswap V3 Slot0\n\nFor MEV bots, reading pool state quickly is essential:\n\n```solidity\nfunction getSlot0(address pool) external view returns (\n    uint160 sqrtPriceX96,\n    int24 tick,\n    uint16 observationIndex,\n    uint16 observationCardinality,\n    uint16 observationCardinalityNext,\n    uint8 feeProtocol,\n    bool unlocked\n) {\n    assembly {\n        \u002F\u002F Prepare staticcall to pool.slot0()\n        mstore(0x00, 0x3850c7bd00000000000000000000000000000000000000000000000000000000)\n\n        \u002F\u002F Execute staticcall\n        let success := staticcall(gas(), pool, 0x00, 0x04, 0x00, 0xe0)\n        if iszero(success) { revert(0, 0) }\n\n        \u002F\u002F Read return data directly from memory\n        sqrtPriceX96 := mload(0x00)\n        tick := mload(0x20)\n        observationIndex := mload(0x40)\n        observationCardinality := mload(0x60)\n        observationCardinalityNext := mload(0x80)\n        feeProtocol := mload(0xa0)\n        unlocked := mload(0xc0)\n    }\n}\n```\n\n## Yul vs Huff: Where Yul Falls Short\n\nYul is a significant step toward the metal, but it is not the final step. Compared to Huff:\n\n| Feature | Yul | Huff |\n|---------|-----|------|\n| Stack management | Automatic (compiler) | Manual |\n| Variable names | Yes | No (stack positions) |\n| Functions | Yes | Macros |\n| Control flow | if\u002Fswitch\u002Ffor | JUMPI\u002FJUMPDEST |\n| Code size control | Limited | Full |\n| Gas overhead | Small (variable management) | Zero |\n| Learning curve | Moderate | Steep |\n\nFor most gas-optimization work, Yul provides 80-90% of the savings with significantly better readability. Huff is reserved for the most extreme cases: ERC-20 implementations, MEV bot cores, and mathematical libraries where every single gas unit matters.\n\n## Common Pitfalls\n\n### 1. Forgetting Memory Is Not Zeroed\n\nYul does not zero-initialize memory. If you read from a memory offset before writing to it, you get whatever was there before:\n\n```yul\n\u002F\u002F BUG: memory at 0x100 may contain garbage\nlet value := mload(0x100)  \u002F\u002F Could be anything!\n```\n\n### 2. Misunderstanding Return Data\n\nAfter a CALL, return data is only valid until the next CALL. If you need the data, copy it to memory immediately:\n\n```yul\nlet success := call(gas(), target, 0, 0, 0, 0, 0)\n\u002F\u002F returndata is available NOW\nlet size := returndatasize()\nreturndatacopy(0x00, 0x00, size)\n\u002F\u002F After the NEXT call, this returndata is gone\n```\n\n### 3. Stack Too Deep in Large Functions\n\nYul manages the stack for you, but the EVM stack is only 1024 elements deep with DUP\u002FSWAP limited to 16. Large functions with many variables will fail to compile.\n\nThe fix: break large functions into smaller ones, or use memory to store intermediate values.\n\n### 4. Not Handling Dirty Upper Bits\n\nWhen you read an `address` from calldata in Yul, the upper 96 bits may be dirty (nonzero). Always mask:\n\n```yul\nlet addr := calldataload(4)  \u002F\u002F 32 bytes, but address is only 20 bytes\naddr := and(addr, 0xffffffffffffffffffffffffffffffffffffffff)  \u002F\u002F Mask to 20 bytes\n```\n\n## Conclusion\n\nYul is the bridge between Solidity's safety and the EVM's raw power. It gives you opcode-level control with variable names, functions, and structured control flow. For MEV developers, mastering Yul is the difference between a profitable bot and one that loses to competitors on gas efficiency.\n\nIn the next article, we dive deep into Yul's memory management: the free memory pointer, manual ABI encoding, and building external call data from scratch.","\u003Ch2 id=\"what-is-yul-and-why-does-it-exist\">What Is Yul and Why Does It Exist\u003C\u002Fh2>\n\u003Cp>Yul is an intermediate language designed by the Solidity team that compiles to EVM bytecode (and, in theory, to eWASM). It sits between Solidity and raw opcodes: you get direct access to every EVM opcode through a readable, structured syntax with variables, functions, if\u002Fswitch\u002Ffor constructs, and scoped blocks.\u003C\u002Fp>\n\u003Cp>Yul exists because Solidity’s compiler generates conservative bytecode. Overflow checks, ABI encoding, memory management — all useful for application contracts but unacceptable overhead for gas-critical code like MEV bots, DEX routers, or on-chain math libraries.\u003C\u002Fp>\n\u003Cp>When you write \u003Ccode>assembly { ... }\u003C\u002Fcode> inside Solidity, you are writing Yul.\u003C\u002Fp>\n\u003Ch2 id=\"yul-syntax-fundamentals\">Yul Syntax Fundamentals\u003C\u002Fh2>\n\u003Cp>Yul’s syntax is minimal. There are no types — everything is a 256-bit word. There are no arrays, no structs, no inheritance. You have:\u003C\u002Fp>\n\u003Ch3>Variables\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-yul\">let x := 42\nlet y := add(x, 1)    \u002F\u002F y = 43\nlet z := mul(x, y)    \u002F\u002F z = 42 * 43 = 1806\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Variables are scoped to the enclosing block \u003Ccode>{ }\u003C\u002Fcode>. They live on the stack. The Yul compiler manages stack layout for you (unlike Huff, where you manage the stack manually).\u003C\u002Fp>\n\u003Ch3>Functions\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-yul\">function safeAdd(a, b) -&gt; result {\n    result := add(a, b)\n    if lt(result, a) {\n        revert(0, 0)  \u002F\u002F overflow\n    }\n}\n\nlet sum := safeAdd(100, 200)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Functions can have multiple return values:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">function divmod(a, b) -&gt; quotient, remainder {\n    quotient := div(a, b)\n    remainder := mod(a, b)\n}\n\nlet q, r := divmod(17, 5)  \u002F\u002F q = 3, r = 2\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Control Flow\u003C\u002Fh3>\n\u003Cp>\u003Cstrong>If\u003C\u002Fstrong> (no else in Yul!):\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">if iszero(calldatasize()) {\n    revert(0, 0)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>Switch\u003C\u002Fstrong> (the else equivalent):\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">switch selector\ncase 0xa9059cbb {  \u002F\u002F transfer(address,uint256)\n    \u002F\u002F handle transfer\n}\ncase 0x70a08231 {  \u002F\u002F balanceOf(address)\n    \u002F\u002F handle balanceOf\n}\ndefault {\n    revert(0, 0)\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cstrong>For\u003C\u002Fstrong> loops:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">for { let i := 0 } lt(i, 10) { i := add(i, 1) } {\n    \u002F\u002F loop body\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"inline-assembly-in-solidity\">Inline Assembly in Solidity\u003C\u002Fh2>\n\u003Cp>The most common way to use Yul is inside Solidity’s \u003Ccode>assembly { }\u003C\u002Fcode> blocks. Solidity variables are accessible within the block:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">function getBalance(address account) external view returns (uint256 bal) {\n    bytes32 slot;\n    assembly {\n        \u002F\u002F balances mapping is at storage slot 0\n        \u002F\u002F balances[account] = keccak256(account . slot_0)\n        mstore(0x00, account)\n        mstore(0x20, 0)  \u002F\u002F slot number\n        slot := keccak256(0x00, 0x40)\n        bal := sload(slot)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This bypasses Solidity’s mapping accessor, saving gas from ABI encoding and bounds checks.\u003C\u002Fp>\n\u003Ch3>Reading and Writing Storage Directly\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-solidity\">contract DirectStorage {\n    \u002F\u002F slot 0: owner (address, 20 bytes)\n    \u002F\u002F slot 1: totalSupply (uint256)\n    \u002F\u002F slot 2: balances mapping base\n\n    function getOwner() external view returns (address o) {\n        assembly {\n            o := sload(0)  \u002F\u002F Read slot 0 directly\n        }\n    }\n\n    function unsafeSetOwner(address newOwner) external {\n        assembly {\n            \u002F\u002F No access control! For demonstration only.\n            sstore(0, newOwner)  \u002F\u002F Write slot 0 directly\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Emitting Events in Yul\u003C\u002Fh3>\n\u003Cp>Events in Yul are emitted using LOG opcodes (LOG0 through LOG4, where the number is the count of indexed topics):\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">event Transfer(address indexed from, address indexed to, uint256 value);\n\nfunction emitTransfer(address from, address to, uint256 value) internal {\n    assembly {\n        \u002F\u002F Transfer(address,address,uint256) topic:\n        let sig := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\n\n        \u002F\u002F Store non-indexed data in memory\n        mstore(0x00, value)\n\n        \u002F\u002F LOG3: 3 topics (event sig + 2 indexed params) + data\n        log3(\n            0x00,    \u002F\u002F memory offset of data\n            0x20,    \u002F\u002F data length (32 bytes)\n            sig,     \u002F\u002F topic 0: event signature\n            from,    \u002F\u002F topic 1: indexed from\n            to       \u002F\u002F topic 2: indexed to\n        )\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This saves gas compared to Solidity’s \u003Ccode>emit Transfer(from, to, value)\u003C\u002Fcode> because we skip ABI encoding overhead.\u003C\u002Fp>\n\u003Ch2 id=\"when-to-use-yul-and-when-not-to\">When to Use Yul (and When Not To)\u003C\u002Fh2>\n\u003Ch3>Use Yul When:\u003C\u002Fh3>\n\u003Col>\n\u003Cli>\u003Cstrong>Gas is critical\u003C\u002Fstrong> — MEV bots, DEX aggregator routers, high-frequency on-chain operations\u003C\u002Fli>\n\u003Cli>\u003Cstrong>You need bitwise operations\u003C\u002Fstrong> — Packing\u002Funpacking data, custom encoding\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Memory layout matters\u003C\u002Fstrong> — Building calldata for external calls, custom ABI encoding\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Solidity generates suboptimal code\u003C\u002Fstrong> — Certain patterns (like copying between memory regions) compile poorly\u003C\u002Fli>\n\u003Cli>\u003Cstrong>You need transient storage\u003C\u002Fstrong> — TLOAD\u002FTSTORE before Solidity adds native support\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Proxy contracts\u003C\u002Fstrong> — The minimal proxy (EIP-1167) is 45 bytes of hand-optimized Yul\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch3>Do Not Use Yul When:\u003C\u002Fh3>\n\u003Col>\n\u003Cli>\u003Cstrong>Readability matters more than gas\u003C\u002Fstrong> — Most application contracts\u003C\u002Fli>\n\u003Cli>\u003Cstrong>You are not confident in your EVM knowledge\u003C\u002Fstrong> — Yul has no safety nets\u003C\u002Fli>\n\u003Cli>\u003Cstrong>The gas savings are negligible\u003C\u002Fstrong> — If saving 200 gas on a 500,000 gas function, it is not worth the audit risk\u003C\u002Fli>\n\u003Cli>\u003Cstrong>You need complex data structures\u003C\u002Fstrong> — Yul has no arrays, structs, or mappings at the language level\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2 id=\"practical-example-efficient-multicall\">Practical Example: Efficient Multicall\u003C\u002Fh2>\n\u003Cp>Here is a real-world example — a gas-efficient multicall function that executes multiple calls in a single transaction:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">function multicall(bytes[] calldata calls) external returns (bytes[] memory results) {\n    results = new bytes[](calls.length);\n    for (uint256 i = 0; i &lt; calls.length;) {\n        (bool success, bytes memory result) = address(this).delegatecall(calls[i]);\n        require(success);\n        results[i] = result;\n        unchecked { ++i; }\n    }\n}\n\n\u002F\u002F Yul-optimized version:\nfunction multicallOptimized(bytes[] calldata calls) external {\n    assembly {\n        let len := calls.length\n        let dataOffset := calls.offset\n\n        for { let i := 0 } lt(i, len) { i := add(i, 1) } {\n            \u002F\u002F Get the offset and length of calls[i]\n            let callOffset := add(dataOffset, calldataload(add(dataOffset, mul(i, 0x20))))\n            let callLen := calldataload(callOffset)\n            let callData := add(callOffset, 0x20)\n\n            \u002F\u002F Copy calldata to memory\n            calldatacopy(0x00, callData, callLen)\n\n            \u002F\u002F Execute delegatecall\n            let success := delegatecall(gas(), address(), 0x00, callLen, 0x00, 0x00)\n            if iszero(success) {\n                returndatacopy(0x00, 0x00, returndatasize())\n                revert(0x00, returndatasize())\n            }\n        }\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"practical-example-reading-uniswap-v3-slot0\">Practical Example: Reading Uniswap V3 Slot0\u003C\u002Fh2>\n\u003Cp>For MEV bots, reading pool state quickly is essential:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-solidity\">function getSlot0(address pool) external view returns (\n    uint160 sqrtPriceX96,\n    int24 tick,\n    uint16 observationIndex,\n    uint16 observationCardinality,\n    uint16 observationCardinalityNext,\n    uint8 feeProtocol,\n    bool unlocked\n) {\n    assembly {\n        \u002F\u002F Prepare staticcall to pool.slot0()\n        mstore(0x00, 0x3850c7bd00000000000000000000000000000000000000000000000000000000)\n\n        \u002F\u002F Execute staticcall\n        let success := staticcall(gas(), pool, 0x00, 0x04, 0x00, 0xe0)\n        if iszero(success) { revert(0, 0) }\n\n        \u002F\u002F Read return data directly from memory\n        sqrtPriceX96 := mload(0x00)\n        tick := mload(0x20)\n        observationIndex := mload(0x40)\n        observationCardinality := mload(0x60)\n        observationCardinalityNext := mload(0x80)\n        feeProtocol := mload(0xa0)\n        unlocked := mload(0xc0)\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"yul-vs-huff-where-yul-falls-short\">Yul vs Huff: Where Yul Falls Short\u003C\u002Fh2>\n\u003Cp>Yul is a significant step toward the metal, but it is not the final step. Compared to Huff:\u003C\u002Fp>\n\u003Ctable>\u003Cthead>\u003Ctr>\u003Cth>Feature\u003C\u002Fth>\u003Cth>Yul\u003C\u002Fth>\u003Cth>Huff\u003C\u002Fth>\u003C\u002Ftr>\u003C\u002Fthead>\u003Ctbody>\n\u003Ctr>\u003Ctd>Stack management\u003C\u002Ftd>\u003Ctd>Automatic (compiler)\u003C\u002Ftd>\u003Ctd>Manual\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Variable names\u003C\u002Ftd>\u003Ctd>Yes\u003C\u002Ftd>\u003Ctd>No (stack positions)\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Functions\u003C\u002Ftd>\u003Ctd>Yes\u003C\u002Ftd>\u003Ctd>Macros\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Control flow\u003C\u002Ftd>\u003Ctd>if\u002Fswitch\u002Ffor\u003C\u002Ftd>\u003Ctd>JUMPI\u002FJUMPDEST\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Code size control\u003C\u002Ftd>\u003Ctd>Limited\u003C\u002Ftd>\u003Ctd>Full\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Gas overhead\u003C\u002Ftd>\u003Ctd>Small (variable management)\u003C\u002Ftd>\u003Ctd>Zero\u003C\u002Ftd>\u003C\u002Ftr>\n\u003Ctr>\u003Ctd>Learning curve\u003C\u002Ftd>\u003Ctd>Moderate\u003C\u002Ftd>\u003Ctd>Steep\u003C\u002Ftd>\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\n\u003Cp>For most gas-optimization work, Yul provides 80-90% of the savings with significantly better readability. Huff is reserved for the most extreme cases: ERC-20 implementations, MEV bot cores, and mathematical libraries where every single gas unit matters.\u003C\u002Fp>\n\u003Ch2 id=\"common-pitfalls\">Common Pitfalls\u003C\u002Fh2>\n\u003Ch3>1. Forgetting Memory Is Not Zeroed\u003C\u002Fh3>\n\u003Cp>Yul does not zero-initialize memory. If you read from a memory offset before writing to it, you get whatever was there before:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">\u002F\u002F BUG: memory at 0x100 may contain garbage\nlet value := mload(0x100)  \u002F\u002F Could be anything!\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>2. Misunderstanding Return Data\u003C\u002Fh3>\n\u003Cp>After a CALL, return data is only valid until the next CALL. If you need the data, copy it to memory immediately:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">let success := call(gas(), target, 0, 0, 0, 0, 0)\n\u002F\u002F returndata is available NOW\nlet size := returndatasize()\nreturndatacopy(0x00, 0x00, size)\n\u002F\u002F After the NEXT call, this returndata is gone\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>3. Stack Too Deep in Large Functions\u003C\u002Fh3>\n\u003Cp>Yul manages the stack for you, but the EVM stack is only 1024 elements deep with DUP\u002FSWAP limited to 16. Large functions with many variables will fail to compile.\u003C\u002Fp>\n\u003Cp>The fix: break large functions into smaller ones, or use memory to store intermediate values.\u003C\u002Fp>\n\u003Ch3>4. Not Handling Dirty Upper Bits\u003C\u002Fh3>\n\u003Cp>When you read an \u003Ccode>address\u003C\u002Fcode> from calldata in Yul, the upper 96 bits may be dirty (nonzero). Always mask:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yul\">let addr := calldataload(4)  \u002F\u002F 32 bytes, but address is only 20 bytes\naddr := and(addr, 0xffffffffffffffffffffffffffffffffffffffff)  \u002F\u002F Mask to 20 bytes\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"conclusion\">Conclusion\u003C\u002Fh2>\n\u003Cp>Yul is the bridge between Solidity’s safety and the EVM’s raw power. It gives you opcode-level control with variable names, functions, and structured control flow. For MEV developers, mastering Yul is the difference between a profitable bot and one that loses to competitors on gas efficiency.\u003C\u002Fp>\n\u003Cp>In the next article, we dive deep into Yul’s memory management: the free memory pointer, manual ABI encoding, and building external call data from scratch.\u003C\u002Fp>\n","en","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:22.681610Z","Practical introduction to Yul assembly: syntax, inline assembly in Solidity, when to use it, and hands-on examples for gas-critical smart contracts.","Yul assembly language",null,"index, follow",[21,26,30,34],{"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",{"id":35,"name":36,"slug":37,"created_at":25},"c0000000-0000-0000-0000-000000000018","Yul","yul","Blockchain",[40,46,52],{"id":41,"title":42,"slug":43,"excerpt":44,"locale":12,"category_name":38,"published_at":45},"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":47,"title":48,"slug":49,"excerpt":50,"locale":12,"category_name":38,"published_at":51},"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":53,"title":54,"slug":55,"excerpt":56,"locale":12,"category_name":38,"published_at":57},"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":59,"slug":60,"bio":61,"photo_url":18,"linkedin":18,"role":62,"created_at":63,"updated_at":63},"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"]