
{"id":126069,"date":"2026-01-10T06:06:41","date_gmt":"2026-01-10T06:06:41","guid":{"rendered":"https:\/\/mycryptomania.com\/?p=126069"},"modified":"2026-01-10T06:06:41","modified_gmt":"2026-01-10T06:06:41","slug":"yul-soliditys-low-level-language-without-the-tears-part-1-stack-memory-and-calldata","status":"publish","type":"post","link":"https:\/\/mycryptomania.com\/?p=126069","title":{"rendered":"YUL: Solidity\u2019s Low-Level Language (Without the Tears), Part 1: Stack, Memory, and Calldata"},"content":{"rendered":"<p>This is a 3-part series that assumes you know Solidity and want to understand YUL. We will start from absolute basics and build up to writing real contracts.<\/p>\n<p>YUL is a low-level language that compiles to EVM bytecode. When you write Solidity, the compiler turns it into YUL, then into bytecode. Writing YUL directly gives you precise control over what the EVM executes.<\/p>\n<p><strong>Code Repository<\/strong>: All code examples from this guide are available in the <a href=\"https:\/\/github.com\/0xKurt\/Solidity-YUL-examples\">YUL Examples repository.<\/a> Each part has its own directory with working examples you can compile and\u00a0test.<\/p>\n<h3>What YUL Is (Briefly)<\/h3>\n<p>YUL is a human-readable, low-level intermediate language that compiles to EVM bytecode. It is what Solidity compiles to before becoming the bytecode that runs on Ethereum.<\/p>\n<p><strong>Important distinction<\/strong>: YUL is <em>not<\/em> raw EVM assembly. It is a structured intermediate representation (IR) that compiles to EVM opcodes. YUL provides higher-level constructs (functions, variables, control flow) that are then compiled down to the stack-based EVM\u00a0opcodes.<\/p>\n<p>You can write YUL in two\u00a0ways:<\/p>\n<p><strong>Inline assembly<\/strong> inside Solidity contracts (using assembly { }\u00a0blocks)<strong>Standalone YUL files<\/strong> (.yul files that compile directly to bytecode)<\/p>\n<p>This guide covers <strong>both approaches<\/strong>. We start with inline assembly to bridge from Solidity, then show standalone YUL contracts.<\/p>\n<h3>How to Think in\u00a0YUL<\/h3>\n<p>Before diving into syntax, understand these core principles:<\/p>\n<p><strong>Everything is a 256-bit word<\/strong>: All values are 32-byte words, no exceptions<strong>All operations are stack-based<\/strong>: Operations push and pop values from the\u00a0stack<strong>Nothing is implicit<\/strong>: No ABI encoding\/decoding, no safety checks, no type\u00a0system<strong>Memory and calldata must be managed manually<\/strong>: You decide where to read from and write\u00a0to<\/p>\n<p>This mental model will help you understand why YUL code looks the way it\u00a0does.<\/p>\n<h3>Basic Syntax<\/h3>\n<p>YUL syntax is\u00a0minimal:<\/p>\n<p><strong>Comments<\/strong>: \/\/ Single-line or \/* Multi-line *\/<strong>Literals<\/strong>: Numbers as decimal (42) or hex\u00a0(0x2A)<strong>Blocks<\/strong>: Code in curly braces {\u00a0}<strong>Statements<\/strong>: Separated by newlines or semicolons<\/p>\n<h3>The Stack: The Foundation<\/h3>\n<p>The EVM is a <strong>stack-based machine<\/strong>. All operations work with values on a stack. You push values, operate on them, and pop them off. The stack has a maximum depth of 1024\u00a0items.<\/p>\n<h3>How the Stack\u00a0Works<\/h3>\n<p>5        \/\/ Push 5 \u2192 stack: [5]<br \/>3        \/\/ Push 3 \u2192 stack: [5, 3]<br \/>add      \/\/ Pop 3 and 5, add them, push 8 \u2192 stack: [8]<\/p>\n<p><strong>Visualization:<\/strong><\/p>\n<p>Initial:  []<br \/>After 5:  [5]<br \/>After 3:  [5, 3]<br \/>After add: [8]<\/p>\n<h3>Common Stack Operations<\/h3>\n<p><strong>dup1, <\/strong><strong>dup2,\u00a0&#8230; <\/strong><strong>dup16<\/strong>: Duplicate value from\u00a0position<strong>swap1, <\/strong><strong>swap2,\u00a0&#8230; <\/strong><strong>swap16<\/strong>: Swap top with value at\u00a0position<strong>pop<\/strong>: Remove top\u00a0value<\/p>\n<h3>Arithmetic Operations<\/h3>\n<p>add     \/\/ Addition<br \/>sub     \/\/ Subtraction (second &#8211; top)<br \/>mul     \/\/ Multiplication<br \/>div     \/\/ Division (second \/ top)<br \/>mod     \/\/ Modulo (second % top)<\/p>\n<p><strong>Important<\/strong>: For sub, div, and mod, the operation is second_value op top_value.<\/p>\n<p><strong>Example:<\/strong><\/p>\n<p>\/\/ Stack: [10, 3]<br \/>sub  \/\/ 10 &#8211; 3 = 7 (result: [7])<br \/>div  \/\/ 10 \/ 3 = 3 (result: [3])<br \/>mod  \/\/ 10 % 3 = 1 (result: [1])<\/p>\n<h3>Comparison Operations<\/h3>\n<p>eq      \/\/ Equal (returns 1 if equal, 0 if not)<br \/>lt      \/\/ Less than (second &lt; top)<br \/>gt      \/\/ Greater than (second &gt; top)<br \/>iszero  \/\/ Is zero (returns 1 if top is 0)<\/p>\n<h3>Logical Operations<\/h3>\n<p>and     \/\/ Bitwise AND (commonly used as logical when values are 0 or 1; YUL does not have a boolean type)<br \/>or      \/\/ Bitwise OR (commonly used as logical when values are 0 or 1; YUL does not have a boolean type)<br \/>xor     \/\/ XOR<br \/>not     \/\/ Bitwise NOT (flips all 256 bits)<\/p>\n<h3>Bit Shift Operations<\/h3>\n<p>Bit shifting moves bits left or right in a\u00a0number:<\/p>\n<p><strong>shl(bits, value)<\/strong>: Shift left (multiply by\u00a02^bits)<\/p>\n<p>Moves all bits to the left, fills right with\u00a0zerosshl(1, 5) = 5 &lt;&lt; 1 = 10 (binary: 101 \u2192\u00a01010)<\/p>\n<p><strong>shr(bits, value)<\/strong>: Shift right (divide by\u00a02^bits)<\/p>\n<p>Moves all bits to the right, fills left with\u00a0zerosshr(1, 10) = 10 &gt;&gt; 1 = 5 (binary: 1010 \u2192\u00a0101)<\/p>\n<p><strong>Why shifting\u00a0matters:<\/strong><\/p>\n<p>Extract specific parts of a\u00a0valuePosition bits for packing\/unpackingRemove unwanted data from the left or\u00a0right<\/p>\n<h3>Data Types<\/h3>\n<p>YUL has only one data type: <strong>256-bit unsigned integers<\/strong> (u256). No strings, arrays, structs, or booleans. Use 0 for false, 1 for\u00a0true.<\/p>\n<h3>From Stack Operations to Functions<\/h3>\n<p>Inline assembly still uses the stack underneath. When you write add(a, b) in an assembly block, Solidity automatically places a and b on the stack, then add pops both values, adds them, and pushes the result. This is the same stack operations as the raw 5, 3, add\u00a0example.<\/p>\n<p>The difference is syntax: inline assembly lets you use function-like syntax while the stack operations happen automatically. In standalone YUL, you are responsible for value lifetimes and ordering, even though YUL provides expression syntax (like add(x, y)) that abstracts individual dup and swap instructions.<\/p>\n<h3>Inline Assembly: Your Bridge from\u00a0Solidity<\/h3>\n<p>The easiest way to start with YUL is using <strong>inline assembly<\/strong> inside Solidity contracts. This lets you write YUL code within familiar Solidity\u00a0syntax.<\/p>\n<h3>Your First Inline\u00a0Assembly<\/h3>\n<p>Let us start with a simple Solidity function and convert it to inline assembly:<\/p>\n<p><strong>Solidity version:<\/strong><\/p>\n<p>function add(uint256 a, uint256 b) public pure returns (uint256) {<br \/>    return a + b;<br \/>}<\/p>\n<p><strong>Inline assembly\u00a0version:<\/strong><\/p>\n<p>function add(uint256 a, uint256 b) public pure returns (uint256 result) {<br \/>    assembly {<br \/>        result := add(a, b)<br \/>    }<br \/>}<\/p>\n<p><strong>What changed:<\/strong><\/p>\n<p>The assembly { } block contains YUL\u00a0codeadd(a, b) is the YUL addition operationresult\u00a0:= assigns the result to the return\u00a0variableSolidity still handles function parameters and return values automatically<\/p>\n<p><strong>Note<\/strong>: You can also define variables inside the assembly block using\u00a0let:<\/p>\n<p>assembly {<br \/>    let sum := add(a, b)  \/\/ Define variable inside assembly<br \/>    \/\/ Use sum here<br \/>}<\/p>\n<p><strong>Why this is\u00a0useful:<\/strong><\/p>\n<p>You can optimize specific functionsYou learn YUL graduallyYou keep Solidity\u2019s safety features (function selectors, ABI encoding)<\/p>\n<h3>Memory: Where Return Values\u00a0Live<\/h3>\n<p>Memory is a temporary byte array used to store intermediate values and return data. It is not persisted between\u00a0calls.<\/p>\n<h3>Memory Layout<\/h3>\n<p>Memory is organized in <strong>32-byte words<\/strong>. Address 0 = first 32 bytes, address 32 = next 32 bytes, and so\u00a0on.<\/p>\n<h3>Memory Operations<\/h3>\n<p><strong>mstore(position, value)<\/strong>: Stores a 256-bit value at a memory\u00a0position<\/p>\n<p>mstore(0, 42)   \/\/ Store 42 at position 0-31<br \/>mstore(32, 100) \/\/ Store 100 at position 32-63<\/p>\n<p><strong>mload(position)<\/strong>: Loads a 256-bit value from\u00a0memory<\/p>\n<p>mstore(0, 42)<br \/>let value := mload(0)  \/\/ value is now 42<\/p>\n<p><strong>Why we need\u00a0memory:<\/strong><\/p>\n<p>To return values from functionsTo prepare data for function\u00a0callsTo work with temporary data<\/p>\n<p><strong>Important note for inline assembly in Solidity:<\/strong><\/p>\n<p><strong>For learning clarity, we use memory slot <\/strong><strong>0 in examples.<\/strong> Production Solidity inline assembly should allocate memory using the free memory\u00a0pointer.<\/p>\n<p><strong>What is scratch\u00a0space?<\/strong><\/p>\n<p>Solidity uses memory positions 0 to 0x3F (64 bytes) as &#8220;scratch space&#8221; for temporary operationsSolidity may overwrite data you store in these positions at any\u00a0timeUsing mstore(0,\u00a0&#8230;) in production code is unsafe because Solidity might overwrite it<\/p>\n<p><strong>What is the free memory\u00a0pointer?<\/strong><\/p>\n<p>Solidity tracks where free memory starts at position\u00a00x40The value stored at 0x40 tells you the next available memory\u00a0addressThis is how Solidity knows where to safely allocate new\u00a0memory<\/p>\n<p><strong>How to use it in production:<\/strong><\/p>\n<p>assembly {<br \/>    \/\/ Get the current free memory pointer<br \/>    let freeMemPtr := mload(0x40)<\/p>\n<p>    \/\/ Use this address for your data<br \/>    mstore(freeMemPtr, yourData)<\/p>\n<p>    \/\/ Update the free memory pointer (move it forward by 32 bytes)<br \/>    mstore(0x40, add(freeMemPtr, 32))<br \/>}<\/p>\n<p>For standalone YUL contracts, you have full control and can use any memory position.<\/p>\n<h3>Memory Alignment<\/h3>\n<p><strong>Important<\/strong>: mstore and mload always operate on 32 bytes, regardless of the starting position.<\/p>\n<p>If you use unaligned addresses (not multiples of 32), you can overwrite adjacent\u00a0data:<\/p>\n<p>mstore(7, 42)   \/\/ Writes 32 bytes starting at byte 7 \u2192 writes to bytes 7-38<br \/>mstore(18, 100) \/\/ Writes 32 bytes starting at byte 18 \u2192 writes to bytes 18-49<br \/>\/\/ These writes overlap! Bytes 18-38 are overwritten by both operations.<\/p>\n<p><strong>Problems with unaligned writes:<\/strong><\/p>\n<p>Overwrites adjacent data (writes span word boundaries)mload reads 32 bytes, so you might read overlapping dataUnpredictable results when reading\u00a0back<\/p>\n<p><strong>Best practice<\/strong>: Always use aligned addresses (0, 32, 64, 96,\u00a0\u2026) for mstore and mload. Use mstore8 if you need byte-level writes.<\/p>\n<h3>Return Operations<\/h3>\n<p>To return data from a function, you\u00a0must:<\/p>\n<p>Store the data in\u00a0memoryUse return(offset, size) to return\u00a0it<\/p>\n<p><strong>return(offset, size)<\/strong>: Returns size bytes starting from memory position\u00a0offset<\/p>\n<p>let result := 42<br \/>mstore(0, result)    \/\/ Store result in memory<br \/>return(0, 32)        \/\/ Return 32 bytes from position 0<\/p>\n<p><strong>\u26a0\ufe0f Important for inline assembly<\/strong>: In inline assembly, return() immediately exits the entire Solidity function and returns raw bytes, bypassing Solidity&#8217;s normal return handling. Use it only when you intend to fully control the return\u00a0data.<\/p>\n<p><strong>Complete example:<\/strong><\/p>\n<p>function getValue() public pure returns (uint256) {<br \/>    assembly {<br \/>        let value := 42<br \/>        mstore(0, value)<br \/>        return(0, 32)  \/\/ Exits function immediately, returns raw bytes<br \/>    }<br \/>}<\/p>\n<h3>Calldata: Reading Function\u00a0Inputs<\/h3>\n<p>Calldata is the input data sent with a transaction. It contains the function selector (first 4 bytes) and function parameters (ABI-encoded).<\/p>\n<h3>Calldata Layout<\/h3>\n<p>For a function like transfer(address to, uint256\u00a0amount):<\/p>\n<p>Bytes 0\u20133: Function\u00a0selectorBytes 4\u201335: to (address, padded to 32\u00a0bytes)Bytes 36\u201367: amount (uint256)<\/p>\n<h3>Calldata Operations<\/h3>\n<p><strong>calldataload(position)<\/strong>: Loads 32 bytes from\u00a0calldata<\/p>\n<p>let selector := calldataload(0)  \/\/ Loads bytes 0-31<\/p>\n<p><strong>Important<\/strong>: Production YUL code should check calldatasize() before reading parameters to avoid out-of-bounds reads. The examples in this guide assume calldata is long enough for demonstration purposes.<\/p>\n<p><strong>Extracting the function selector:<\/strong><\/p>\n<p>calldataload(0) loads 32 bytes, but the function selector is only the first 4 bytes. We need to extract just those 4\u00a0bytes.<\/p>\n<p><strong>The problem:<\/strong><\/p>\n<p>Calldata: [selector (4 bytes)][padding (28\u00a0bytes)]calldataload(0) returns: [selector][padding] (all 32\u00a0bytes)We want: just the selector (4\u00a0bytes)<\/p>\n<p><strong>The solution: Shift right by 224\u00a0bits<\/strong><\/p>\n<p>let selector := shr(224, calldataload(0))  \/\/ Shift right by 224 bits<\/p>\n<p><strong>Why 224\u00a0bits?<\/strong><\/p>\n<p>32 bytes = 256 bits\u00a0totalSelector = 4 bytes = 32\u00a0bitsPadding = 28 bytes = 224\u00a0bitsShift right by 224 bits moves the selector to the rightmost position and removes the\u00a0padding<\/p>\n<p><strong>Visual example:<\/strong><\/p>\n<p>Before shift (32 bytes):<br \/>[selector (4 bytes)][padding (28 bytes)]<br \/>0xa9059cbb00000000000000000000000000000000000000000000000000000000After shr(224, &#8230;):<br \/>[zeros][selector (4 bytes)]<br \/>0x00000000000000000000000000000000000000000000000000000000a9059cbb<br \/>                                                          ^^^^^^^^<br \/>                                                 Just the selector<\/p>\n<p>The selector is now in the rightmost 4 bytes, with zeros on the\u00a0left.<\/p>\n<p><strong>Reading function parameters:<\/strong><\/p>\n<p>\/\/ Read &#8216;to&#8217; (skip 4 bytes for selector)<br \/>let to := calldataload(4)<br \/>\/\/ Mask to get just the address (20 bytes)<br \/>to := and(to, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)\/\/ Read &#8216;amount&#8217; (skip 4 + 32 = 36 bytes)<br \/>let amount := calldataload(36)<\/p>\n<p><strong>Why mask addresses:<\/strong><\/p>\n<p>Addresses are 20 bytes, but calldata pads them to 32\u00a0bytesThe leftmost 12 bytes are\u00a0zerosMasking keeps only the rightmost 20\u00a0bytes<\/p>\n<p><strong>How masking\u00a0works:<\/strong><\/p>\n<p>The and operation performs bitwise AND: it keeps bits where both values are 1, and clears bits where either is\u00a00.<\/p>\n<p>The mask 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF is 20 bytes of all 1s (rightmost 20 bytes). When you and a value with this\u00a0mask:<\/p>\n<p>Leftmost 12 bytes: AND with 0 \u2192 becomes 0 (cleared)Rightmost 20 bytes: AND with 1 \u2192 stays the same\u00a0(kept)<\/p>\n<p><strong>Why masking is necessary:<\/strong><\/p>\n<p>Masking is defensive programming: it ensures we only use the rightmost 20 bytes even if calldata is malformed or contains unexpected values.<\/p>\n<p>In normal cases, addresses are already left-padded with\u00a0zeros:<\/p>\n<p>Input: 0x0000000000000000000000001234567890123456789012345678901234567890<br \/>         ^^^^^^^^^^^^^^^^^^^^^^^^ already zeros          <br \/>                            kept ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^<\/p>\n<p>But what if calldata is malformed or a malicious caller passes non-zero values in the leftmost 12\u00a0bytes?<\/p>\n<p>Malicious input: 0xFFFFFFFFFFFFFFFFFFFFFFFF1234567890123456789012345678901234567890<br \/>                 ^^^^^^^^^^^^^^^^^^^^^^^^^^ garbage (could cause issues)<\/p>\n<p><strong>What could go wrong without\u00a0masking:<\/strong><\/p>\n<p>If you use an unmasked address value directly, the garbage bytes\u00a0could:<\/p>\n<p><strong>Incorrect storage slot calculations<\/strong>: When computing keccak256(address, slot) for mappings, the garbage bytes change the hash, leading to wrong storage\u00a0slots<strong>Security vulnerabilities<\/strong>: An attacker could manipulate storage by crafting addresses with specific leftmost\u00a0bytes<strong>Broken assumptions<\/strong>: Your code might assume addresses are 20 bytes, but unmasked values are 32 bytes with\u00a0garbage<strong>Unexpected behavior<\/strong>: Comparisons, equality checks, and other operations could fail or behave unexpectedly<\/p>\n<p><strong>\u26a0\ufe0f Always mask addresses from calldata.<\/strong><\/p>\n<p><strong>Note<\/strong>: This function is not meant to be realistic. It is a mechanical demonstration of concepts covered in Part\u00a01.<\/p>\n<p>Here is a complete function demonstrating all concepts from Part\u00a01:<\/p>\n<p>function processAddressAndAmount(address addr, uint256 amount) public pure returns (uint256)<br \/>    assembly {<br \/>        \/\/ 1. Extract function selector (demonstrates bit shifting)<br \/>        \/\/ calldataload(0) loads 32 bytes, but selector is only 4 bytes<br \/>        \/\/ Shift right by 224 bits (28 bytes) to move selector to rightmost position<br \/>        let selector := shr(224, calldataload(0))<\/p>\n<p>        \/\/ 2. Read address from calldata (position 4, after selector)<br \/>        let addr := calldataload(4)<\/p>\n<p>        \/\/ 3. Mask address to ensure only 20 bytes (demonstrates defensive programming)<br \/>        \/\/ Addresses are 20 bytes, but calldata pads them to 32 bytes<br \/>        \/\/ Masking clears any potential garbage in the leftmost 12 bytes<br \/>        addr := and(addr, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)<\/p>\n<p>        \/\/ 4. Read uint256 from calldata (position 36, after selector + address)<br \/>        let amount := calldataload(36)<\/p>\n<p>        \/\/ 5. Store values in memory (demonstrates memory operations)<br \/>        mstore(0, addr)      \/\/ Store address at position 0<br \/>        mstore(32, amount)    \/\/ Store amount at position 32<\/p>\n<p>        \/\/ 6. Perform a calculation (e.g., add selector to amount for demonstration)<br \/>        \/\/ In a real contract, you&#8217;d do something meaningful with addr and amount<br \/>        let result := add(selector, amount)<\/p>\n<p>        \/\/ 7. Return the result<br \/>        mstore(0, result)    \/\/ Store result at memory position 0<br \/>        return(0, 32)        \/\/ Return 32 bytes from memory position 0<br \/>    }<br \/>}<\/p>\n<p><strong>Note:<\/strong> The function has parameters (`address addr, uint256 amount`) that Solidity would normally handle automatically. The parameters are intentionally unused in the function body. We read from calldata manually to demonstrate how calldata parsing works. In practice, you would use the parameters directly, but reading from calldata shows the low-level mechanics.<\/p>\n<p><strong>What happens step by\u00a0step:<\/strong><\/p>\n<p><strong>Function selector extraction<\/strong>: shr(224, calldataload(0)) extracts the 4-byte selector from\u00a0calldata<strong>Address reading<\/strong>: calldataload(4) reads the address parameter (32\u00a0bytes)<strong>Address masking<\/strong>: and(&#8230;, mask) ensures only the rightmost 20 bytes are used, clearing any\u00a0garbage<strong>Amount reading<\/strong>: calldataload(36) reads the uint256 parameter<strong>Memory storage<\/strong>: mstore stores both values in memory for potential use<strong>Calculation<\/strong>: Performs an operation (in this example, adds selector to\u00a0amount)<strong>Return<\/strong>: Stores result in memory and returns\u00a0it<\/p>\n<h3>Standalone YUL Contracts<\/h3>\n<p>Standalone YUL files (.yul) compile directly to bytecode. They require more setup but give you full\u00a0control.<\/p>\n<h3>Contract Structure<\/h3>\n<p>A standalone YUL contract has two\u00a0parts:<\/p>\n<p>object &#8220;ContractName&#8221; {<br \/>    code {<br \/>        \/\/ Deployment code &#8211; runs once when contract is created<br \/>        datacopy(0, dataoffset(&#8220;runtime&#8221;), datasize(&#8220;runtime&#8221;))<br \/>        return(0, datasize(&#8220;runtime&#8221;))<br \/>    }<\/p>\n<p>    object &#8220;runtime&#8221; {<br \/>        code {<br \/>            \/\/ Runtime code &#8211; this is the actual contract<br \/>            \/\/ Your functions go here<br \/>        }<br \/>    }<br \/>}<\/p>\n<p><strong>What each part\u00a0does:<\/strong><\/p>\n<p><strong>code block<\/strong>: Runs during deployment. It copies the runtime code and returns it. This is what gets stored on-chain.<strong>runtime code block<\/strong>: The actual contract code that runs on every\u00a0call.<\/p>\n<h3>Deployment Operations<\/h3>\n<p><strong>datacopy(dest, offset, size)<\/strong>: Copies data to\u00a0memory<\/p>\n<p>dest: Memory position to copy\u00a0tooffset: Source data\u00a0offsetsize: Number of bytes to\u00a0copy<\/p>\n<p><strong>dataoffset(&#8220;name&#8221;)<\/strong>: Returns the offset of a named object&#8217;s\u00a0data<\/p>\n<p><strong>datasize(&#8220;name&#8221;)<\/strong>: Returns the size of a named object&#8217;s\u00a0data<\/p>\n<p><strong>Why this structure:<\/strong><\/p>\n<p>The code block sets up the\u00a0contractThe runtime block contains the actual\u00a0logicThis separation is required for standalone YUL contracts<\/p>\n<h3>Complete Standalone YUL\u00a0Example<\/h3>\n<p><strong>Important<\/strong>: This standalone contract does not implement ABI dispatch. Any calldata sent is interpreted as raw arguments (function selector + parameters). <strong>Any call to this contract, regardless of selector, will execute the same code.<\/strong> In a real contract, you would check the function selector and route to different handlers (we\u2019ll cover this in Part\u00a03).<\/p>\n<p>Here is a complete standalone YUL contract demonstrating all Part 1 concepts:<\/p>\n<p>object &#8220;ProcessAddress&#8221; {<br \/>    code {<br \/>        \/\/ Deployment code &#8211; runs once when contract is created<br \/>        \/\/ 1. Copy runtime code to memory using datacopy<br \/>        \/\/    &#8211; Destination: memory position 0<br \/>        \/\/    &#8211; Source: dataoffset(&#8220;runtime&#8221;) &#8211; where the runtime object&#8217;s data starts<br \/>        \/\/    &#8211; Size: datasize(&#8220;runtime&#8221;) &#8211; how many bytes the runtime code is<br \/>        datacopy(0, dataoffset(&#8220;runtime&#8221;), datasize(&#8220;runtime&#8221;))<\/p>\n<p>        \/\/ 2. Return the runtime code (this is what gets stored on-chain)<br \/>        \/\/    &#8211; Offset: 0 (where we copied the code in memory)<br \/>        \/\/    &#8211; Size: datasize(&#8220;runtime&#8221;) (how many bytes to return)<br \/>        return(0, datasize(&#8220;runtime&#8221;))<br \/>    }<\/p>\n<p>    object &#8220;runtime&#8221; {<br \/>        code {<br \/>            \/\/ Runtime code &#8211; this runs on every function call<\/p>\n<p>            \/\/ 1. Extract function selector (demonstrates bit shifting)<br \/>            \/\/    calldataload(0) loads 32 bytes, but selector is only 4 bytes<br \/>            \/\/    Shift right by 224 bits to move selector to rightmost position<br \/>            let selector := shr(224, calldataload(0))<\/p>\n<p>            \/\/ 2. Read address from calldata (position 4, after selector)<br \/>            let addr := calldataload(4)<\/p>\n<p>            \/\/ 3. Mask address to ensure only 20 bytes (demonstrates defensive programming)<br \/>            \/\/    Addresses are 20 bytes, but calldata pads them to 32 bytes<br \/>            \/\/    Masking clears any potential garbage in the leftmost 12 bytes<br \/>            addr := and(addr, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)<\/p>\n<p>            \/\/ 4. Read uint256 from calldata (position 36, after selector + address)<br \/>            let amount := calldataload(36)<\/p>\n<p>            \/\/ 5. Store values in memory (demonstrates memory operations)<br \/>            mstore(0, addr)      \/\/ Store address at position 0<br \/>            mstore(32, amount)   \/\/ Store amount at position 32<\/p>\n<p>            \/\/ 6. Perform a calculation (e.g., add selector to amount)<br \/>            \/\/    In a real contract, you&#8217;d do something meaningful with addr and amount<br \/>            let result := add(selector, amount)<\/p>\n<p>            \/\/ 7. Return the result<br \/>            mstore(0, result)    \/\/ Store result at memory position 0<br \/>            return(0, 32)        \/\/ Return 32 bytes from memory position 0<br \/>        }<br \/>    }<br \/>}<\/p>\n<p><strong>What each part\u00a0does:<\/strong><\/p>\n<p><strong>Deployment (<\/strong><strong>code\u00a0block):<\/strong><\/p>\n<p><strong>datacopy(0, dataoffset(&#8220;runtime&#8221;), datasize(&#8220;runtime&#8221;))<\/strong>:Copies the runtime code from the runtime object into memory at position\u00a00dataoffset(&#8220;runtime&#8221;) returns where the runtime object&#8217;s data starts in the contract\u00a0bytecodedatasize(&#8220;runtime&#8221;) returns the size of the runtime code in\u00a0bytes<\/p>\n<p>2. <strong>return(0, datasize(&#8220;runtime&#8221;))<\/strong>:<\/p>\n<p>Returns the runtime code from\u00a0memoryThis returned code is what gets stored on-chain as the\u00a0contract<\/p>\n<p><strong>Runtime (<\/strong><strong>runtime object&#8217;s <\/strong><strong>code\u00a0block):<\/strong><\/p>\n<p><strong>Function selector extraction<\/strong>: Uses shr(224, calldataload(0)) to extract the 4-byte\u00a0selector<strong>Address reading and masking<\/strong>: Reads address from calldata and masks it to ensure only 20\u00a0bytes<strong>Amount reading<\/strong>: Reads uint256 from\u00a0calldata<strong>Memory operations<\/strong>: Stores values in memory using\u00a0mstore<strong>Calculation<\/strong>: Performs an operation with the\u00a0values<strong>Return<\/strong>: Returns the\u00a0result<\/p>\n<h3>Common Beginner\u00a0Mistakes<\/h3>\n<p>Avoid these common pitfalls when learning\u00a0YUL:<\/p>\n<p><strong>1. Forgetting to mask addresses<\/strong><\/p>\n<p>Addresses from calldata are 32 bytes, but only 20 bytes are\u00a0validAlways mask: addr\u00a0:= and(addr, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)Unmasked addresses can corrupt storage slot calculations<\/p>\n<p><strong>2. Using unaligned <\/strong><strong>mstore addresses<\/strong><\/p>\n<p>mstore always writes 32 bytes, regardless of starting\u00a0positionUse aligned addresses (0, 32, 64, 96,\u00a0\u2026) to avoid overlapping writesUse mstore8 for byte-level writes if\u00a0needed<\/p>\n<p><strong>3. Assuming Solidity safety checks\u00a0exist<\/strong><\/p>\n<p>YUL has no type system, no overflow protection, no bounds\u00a0checkingYou must validate all inputs\u00a0manuallyCheck calldatasize() before reading parameters<\/p>\n<p><strong>4. Using <\/strong><strong>return() in inline assembly without understanding<\/strong><\/p>\n<p>return() in inline assembly bypasses Solidity&#8217;s return\u00a0handlingIt immediately exits the function with raw\u00a0bytesOnly use when you need full control over return\u00a0data<\/p>\n<p><strong>5. Using memory slot <\/strong><strong>0 in production Solidity\u00a0code<\/strong><\/p>\n<p>Memory below 0x40 is scratch space and may be overwrittenAlways use the free memory pointer: mload(0x40)Examples in this guide use slot 0 for learning clarity\u00a0only<\/p>\n<h3>What We\u00a0Learned<\/h3>\n<p><strong>The stack underlies everything<\/strong>: All operations work with the\u00a0stack<strong>Inline assembly bridges Solidity and YUL<\/strong>: Start here to learn gradually<strong>Memory is for temporary data<\/strong>: Use it to store return\u00a0values<strong>Calldata contains function inputs<\/strong>: Read parameters manually<strong>Return requires memory<\/strong>: Store data in memory, then return\u00a0it<strong>Standalone YUL needs structure<\/strong>: code block for deployment, runtime for\u00a0logic<\/p>\n<p>In the next part, we will dive deep into <strong>storage<\/strong>, the persistent database where contract state lives. We\u2019ll see why incorrect storage access is far more dangerous than incorrect memory\u00a0usage.<\/p>\n<p><strong>Questions?<\/strong> Drop a comment below and I\u2019ll do my best to\u00a0help!<\/p>\n<p><strong>Want to stay updated?<\/strong> Follow to get notified when Part 2 is published.<\/p>\n<p><a href=\"https:\/\/medium.com\/coinmonks\/yul-soliditys-low-level-language-without-the-tears-part-1-stack-memory-and-calldata-5b06369ffa3f\">YUL: Solidity\u2019s Low-Level Language (Without the Tears), Part 1: Stack, Memory, and Calldata<\/a> was originally published in <a href=\"https:\/\/medium.com\/coinmonks\">Coinmonks<\/a> on Medium, where people are continuing the conversation by highlighting and responding to this story.<\/p>","protected":false},"excerpt":{"rendered":"<p>This is a 3-part series that assumes you know Solidity and want to understand YUL. We will start from absolute basics and build up to writing real contracts. YUL is a low-level language that compiles to EVM bytecode. When you write Solidity, the compiler turns it into YUL, then into bytecode. Writing YUL directly gives [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":126070,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[],"class_list":["post-126069","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-interesting"],"_links":{"self":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts\/126069"}],"collection":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=126069"}],"version-history":[{"count":0,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/posts\/126069\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=\/wp\/v2\/media\/126070"}],"wp:attachment":[{"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=126069"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=126069"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mycryptomania.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=126069"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}