Yul and Assemly
1. Data types
1.0) Booleans
let isTrue := 1 // Boolean assignment (1 for true, 0 for false)
1.1) Integers
let x := 42 // Integer assignment
1.2) Fixed-size byte arrays
let data := 0x1234567890123456789012345678901234567890123456789012345678901234 // Fixed-size byte array initialization
1.3) Dynamic arrays
let dynamicArray := mload(0x40) // Pointer to dynamically sized array
1.4) Address
let data := 0x1234567890123456789012345678901234567890123456789012345678901234 // Fixed-size byte array initialization
1.5) Tuples
let myTuple := { x: 10, y: 20 } // Tuple initialization
1.6) Functions
function myFunction() {
// Function definition
}
let myFunc := myFunction // Function type assignment
Q. How about strings?
- In YUL/Assembly, strings are typically represented as byte arrays or handled indirectly through pointers.
- YUL doesn't have a built-in string data type like some higher-level languages.
- Strings are often manipulated as arrays of bytes where each byte represents a character in the string.
Here's how strings are usually handled:
let myString := 0x48656c6c6f20576f726c64 // "Hello World" represented in hexadecimal
2. Operations
I. Arithmetic Operations
Addition
a) add: let additionResult := add(42, 8) // Addition operation: 42 + 8 = 50
Subtraction
b) sub: let subtractionResult := sub(100, 25) // Subtraction operation: 100 - 25 = 75
Multiplication
c) mul: let multiplicationResult := mul(6, 7) // Multiplication operation: 6 * 7 = 42
Division
d) div: let divisionResult := div(100, 5) // Division operation: 100 / 5 = 20
Modulo
e) mod: let moduloResult := mod(17, 5) // Modulo operation: 17 % 5 = 2
Exponential Power
f) exp: let exponentialResult := exp(2, 5) // Exponential operation: 2^5 = 32
II. Bitwise Operations
Bitwise AND: "and"
Bitwise OR: "or"
Bitwise XOR: "xor"
Bitwise NOT: "not"
Bitwise Shift Left: "shl"
Bitwise Shift Right: "shr"
III. Storage Operations
Load
a) sload: let valueFromStorage := sload(0x00) // Loading value from storage slot 0x00
Store
b) sstore: sstore(0x00, 42) // Storing value 42 in storage slot 0x00
Memory Load
c) mload: let valueFromMemory := mload(0x40) // Loading value from memory at position 0x40
Memory Store
d) mstore mstore(0x80, 123) // Storing value 123 at memory position 0x80
IV. Control Flow Operations
Conditional Branching
a) jump: jump("myLabel") // Jumping unconditionally to a label named "myLabel"
b) jumptime: jumptime("myLabel") // Jumping to "myLabel" based on a condition being true
c) jumpdest: myLabel: // This is a jump destination used by "jump" or "jumptime"
Function Call
a) call: call(gas, to, value, inOffset, inSize, outOffset, outSize) // Function call with parameters
b) callcode: callcode(gas, to, value, inOffset, inSize, outOffset, outSize) // Call another contract with own code
c) delegatecall: delegatecall(gas, to, inOffset, inSize, outOffset, outSize) // Delegate call to another contract
d) staticcall: staticcall(gas, to, inOffset, inSize, outOffset, outSize) // Static call to another contract
Halting Execution
a) stop: stop // Halts execution
b) return: return(inOffset, inSize) // Returns data from a call
V. Address Operations:
a) address: address // Current contract's address
b) balance: balance(address) // Balance of a specific address
c) origin: origin // Address of the sender of the transaction
d) caller: caller // Address of the caller of the current contract
e) callvalue: callvalue // Amount of ether sent with the call
VI. Miscellaneous Operations:
Log:
a) "log0",
b) "log1",
c) "log2",
d) "log3",
e) "log4"
Self-destruct:
a) "selfdestruct"
Gas Operations:
a) "gas",
b) "gasprice"
3. Storage slots
Storage slots refer to locations where smart contracts can store persistent data.
These slots are akin to a key-value store, where each slot has a unique identifier (key) and can store 256-bit values (value).
Each contract on the Ethereum blockchain has its own storage space, consisting of a virtually unlimited number of slots.
These slots are numbered sequentially, starting from 0.
For instance, a simple example of storing a value in a storage slot and then retrieving it can be represented as:
```assembly
// Storing a value in a storage slot
sstore(0, 42) // Storing value 42 in storage slot 0
// Retrieving the value from the storage slot
let retrievedValue := sload(0) // Loading the value stored in storage slot 0
4. Storage offsets
Storage offsets in the context of Ethereum's Solidity and YUL/Assembly refer to the starting point within a contract's storage where specific data is stored.
Each variable or data structure stored in the contract's storage occupies a certain amount of space, and the storage offset determines where that data begins.
uint256 variable1;
uint256[] variable2;
mapping(uint256 => uint256) variable3;
In this scenario:
- variable1 might occupy a storage slot at offset 0.
- variable2 would likely start at a different storage slot offset, depending on the gas optimization.
- variable3 might start at a different offset as it's a mapping, which can be less predictable.
- However, accessing these variables directly by specifying their storage offsets isn't typically recommended due to readability and optimization reasons.
5. Bitshifting
Bitshifting is commonly used in programming to efficiently perform multiplication or division by powers of 2.
It's crucial for various bitwise operations, especially in optimizing algorithms and data manipulation within smart contracts.
Understanding bitshifting enables developers to perform efficient calculations and manage data at the bit level, contributing to more optimized and cost-effective smart contract development.
Bitshifting in programming refers to the process of moving the bits of a binary number left or right.
Left shifts (<<) move the bits to the left, effectively multiplying the number by 2 for each shift.
- The left shift operator (`<<`) moves the bits of a number to the left.
- For each shift left by `n` bits, the number is effectively multiplied by `2^n`.
// Example of left shift by 2 bits
let shiftedValue := 8 << 2 // This would result in '32' (8 * 2^2)
---
Right shifts (>>) move the bits to the right, effectively dividing the number by 2 for each shift.
- The right shift operator (>>) moves the bits of a number to the right.
- For each shift right by n bits, the number is effectively divided by 2^n.
// Example of right shift by 1 bit
let shiftedValue := 16 >> 1 // This would result in '8' (16 / 2^1)
6. Arrays
Arrays are implemented using contiguous memory spaces. However, the EVM doesn't inherently have a native data structure specifically defined as an "array" like higher-level programming languages do.
Instead, we have to manually manage memory slots to simulate arrays.
Arrays are simulated by managing memory slots and calculating offsets to access elements at specific positions.
To simulate arrays in Yul/Assembly, memory slots are used to store elements, and arithmetic operations calculate offsets to access elements at different positions.
// Example of simulating an array in Yul/Assembly for EVM
{
// Start of array in memory
mstore(0x40, 0) // Set array length at memory position 0x40
// Storing elements in the array
mstore(add(0x40, mul(1, 32)), 123) // Storing value 123 at index 0
mstore(add(0x40, mul(2, 32)), 456) // Storing value 456 at index 1
// Retrieving elements from the array
let retrievedValue1 := mload(add(0x40, mul(1, 32))) // Retrieving value at index 0
let retrievedValue2 := mload(add(0x40, mul(2, 32))) // Retrieving value at index 1
}
In this example:
- The memory at position 0x40 stores the length of the array.
- Values are stored in memory slots calculated based on the array's start (0x40) and index positions.
- Retrieving values involves calculating offsets to access specific elements in memory.
Simulating arrays in Yul/Assembly for the EVM involves managing memory slots and using arithmetic operations to access and manipulate elements.
Understanding this technique is crucial for organizing and efficiently managing data within smart contracts despite the absence of direct array data structures.
While Yul/Assembly doesn't have native array support, this approach enables developers to work with structured data by organizing elements in memory and calculating offsets for element access.
7. Mappings
Mappings in Solidity are a key-value data structure, but in Yul/Assembly for the Ethereum Virtual Machine (EVM), there isn't a direct equivalent.
In lower-level languages like Yul/Assembly, mapping-like behavior can be simulated by using a combination of data structures or by manually managing storage slots.
For example, consider simulating a basic mapping-like structure using storage slots:
// Simulating a basic mapping in Yul/Assembly
{
// Storing values in specific storage slots based on keys
sstore(0, 123) // Storing value 123 with key 0
sstore(1, 456) // Storing value 456 with key 1
// Retrieving values associated with keys
let value1 := sload(0) // Retrieving value associated with key 0
let value2 := sload(1) // Retrieving value associated with key 1
}
In this example:
- sstore is used to store values in specific storage slots, emulating key-value pairs.
- sload is used to retrieve values associated with specific keys.
Simulating mappings in Yul/Assembly for the EVM involves manually managing storage slots and using specific storage positions to store and retrieve key-value pairs.
While this method can mimic mapping-like behavior, it lacks the flexibility and abstraction of Solidity's native mappings.
Understanding how to simulate mapping-like structures in Yul/Assembly is essential for managing data in smart contracts and efficiently organizing key-value pairs within the contract's storage.
8. Mappings
8. Memory
In Ethereum's Solidity, memory is a temporary data location that is more cost-efficient than storage but only exists during the execution of a function.
It is used to store variables that are temporary and do not need to be permanently stored on the blockchain.
In Yul/Assembly for the Ethereum Virtual Machine (EVM), `memory` refers to a temporary data area that functions similarly to Solidity's `memory`.
However, in Yul/Assembly, developers need to explicitly manage and allocate memory space.
// Example of using memory in Yul/Assembly
{
// Allocate memory for 32 bytes
let memPointer := malloc(32)
// Store a value in memory
mstore(memPointer, 123)
// Retrieve the value from memory
let retrievedValue := mload(memPointer)
// Free the allocated memory
mfree(memPointer)
In this example:
- alloc is used to allocate 32 bytes of memory.
- mstore is used to store a value (123 in this case) at the memory location pointed to by memPointer.
- mload retrieves the value stored at the memory location.
- mfree frees the allocated memory space.
Memory in Yul/Assembly serves as a temporary data area where variables can be stored during contract execution. Proper management of memory space is crucial to avoid memory leaks and optimize gas usage.
Understanding how to allocate, store, and retrieve data in memory is essential for efficient data manipulation within smart contracts written in Yul/Assembly
10. Fixed Arguments
Fixed arguments refer to function parameters with a fixed size that is known at compile time.
In Solidity, these are typically variables with fixed data types or structs with fixed structures.
For example, a function with fixed arguments might look like this:
// Example of a function with fixed arguments in Solidity
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
In this example:
- uint256 a and uint256 b are fixed arguments with a known size.
12. Dynamic Arguments
Dynamic arguments refer to function parameters with a variable or dynamic size that might not be known at compile time.
These can include variable-sized arrays, strings, or data structures with variable lengths.
For example, a function with dynamic arguments might involve managing arrays or variable-sized data:
// Example of a function with dynamic arguments in Solidity
function concatenateStrings(string memory str1, string memory str2) public pure returns (string memory) {
return string(abi.encodePacked(str1, str2));
}
In this example:
- string memory str1 and string memory str2 are dynamic arguments that can have variable sizes, and their lengths might not be fixed at compile time.
13. Receiving contract calls
- In Yul, contract calls can be received and processed using functions marked as `external`, enabling contracts to interact with each other or accept messages from external sources.
- Yul functions can be designated as `external` to receive calls from external sources. These functions can handle incoming messages and perform specific actions or logic.
// Example of a Yul contract with a function to receive calls
{
// Define a function to receive and process incoming calls
function receiveCall() external {
// Process incoming call...
// Additional logic...
}
}
14. Storing data in contract bytecode
- In Yul, it's possible to embed data within the contract's creation bytecode during the contract deployment phase.
- However, this technique involves manipulating the contract's initialization code and should be approached with caution.
- The `mstore` instruction in Yul can be used to store data directly within the contract's initialization code.
- This technique allows for embedding specific values or information in the contract's bytecode.
// Example of embedding data in contract bytecode using Yul
{
// Define contract initialization code
code {
// Store data in contract's bytecode using mstore
mstore(0x0, 42) // Store the value 42 at position 0x0 in bytecode
// Additional initialization code...
}
}
In this example:
- mstore is used to store the value 42 at the beginning of the contract's bytecode (0x0 represents the starting position).
- The stored data becomes part of the contract's initialization code.
Caution:
- Embedding data directly within the contract bytecode should be done cautiously. Modifying bytecode after deployment is challenging and can lead to unexpected behavior or vulnerabilities.
15. Solidity Less Smart Contracts
- will add bunch of examples some other time