Skip to content

Instantly share code, notes, and snippets.

@teddav
Last active March 17, 2023 14:53
Show Gist options
  • Save teddav/e5c77d36d76567631ba5898a64a79079 to your computer and use it in GitHub Desktop.
Save teddav/e5c77d36d76567631ba5898a64a79079 to your computer and use it in GitHub Desktop.
Tutorial for writing a basic contract in Yul (assembly). See https://blog.angle.money/playing-with-yul-cd4785e456d8
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.15;
contract AngleExplains {
uint private secretNumber;
mapping(address => uint) public guesses;
bytes32 public secretWord;
function getSecretNumber() external view returns(uint) {
assembly {
// We get the value for secretNumber which is at slot 0
// in Yul, you also have access to the slot number of a variable through `.slot`
// https://docs.soliditylang.org/en/latest/assembly.html#access-to-external-variables-functions-and-libraries
// so we could also just write `sload(secretNumber.slot)`
// SLOAD https://www.evm.codes/#54
let _secretNumber := sload(0)
// then we get the "free memory pointer"
// that means we get the address in the memory where we can write to
// we use the MLOAD opcode for that: https://www.evm.codes/#51
// We get the value stored at 0x40 (64)
// 0x40 is just a constant decided in the EVM where the address of the free memory is stored
// see here: https://docs.soliditylang.org/en/latest/assembly.html#memory-management
let ptr := mload(0x40)
// we write our number at that address
// to do that, we use the MSTORE opcode: https://www.evm.codes/#52
// It takes 2 parameters: the address in memory where to store our value, and the value to store
mstore(ptr, _secretNumber)
// then we RETURN the value: https://www.evm.codes/#f3
// we specify the address where the value is stored: `ptr`
// and the size of the parameter returned: 32 bytes (remember values are always stored on 32 bytes)
return(ptr, 0x20)
// instead of using the free memory pointer, we could also store the value at `0`
// because the first 2 slots in memory are used as "scratch space"
// https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html#layout-in-memory
// this means they are used to store temporary values, such as return values
// we would have had:
// mstore(0, _secretNumber)
// return(0, 0x20)
}
}
function setSecretNumber(uint _number) external {
assembly {
// We get the slot number for `secretNumber`
let slot := secretNumber.slot
// We use SSTORE to store the new value
// https://www.evm.codes/#51
sstore(slot, _number)
}
}
function addGuess(uint _guess) external {
assembly {
// first we compute the slot where we will store the value
// https://solidity-fr.readthedocs.io/fr/latest/internals/layout_in_storage.html#mappings-and-dynamic-arrays
// we have: keccak256(abi.encode(_user, 1)) where 1 is the slot number for `guesses`
let ptr := mload(0x40)
// we store the address of msg.sender at `ptr` address
// CALLER opcode: https://www.evm.codes/#33
mstore(ptr, caller())
// then right after that, we store the slot number for `guesses`
// in Assembly we can't do simple operations (+ - * /)
// we need to use specific opcodes for that
// here we use ADD to add 32 bytes to the address of `ptr`
// this is equivalent to: ptr = ptr + 32
// ADD: https://www.evm.codes/#01
mstore(add(ptr, 0x20), guesses.slot)
// the 2 previous MSTORE are equivalent to abi.encode(msg.sender, 1)
// then we just compute the hash of the msg.Sender and guesses.slot
// they are currently stored at `ptr` and use 2 slots (2x 32bytes -> 0x40)
// KECCAK256 opcode https://www.evm.codes/#20
// still appears as SHA3 on evm.codes which is the old name. Was later renamed as KECCAK256
let slot := keccak256(ptr, 0x40)
// we now only need to store the value at that slot
sstore(slot, _guess)
}
}
// computes the keccak256 hash of a string and stores it in a state variable
function hashSecretWord1(string memory _str) external pure returns(bytes32) {
assembly {
// in assembly `_str` is just a pointer to the string
// it represents the address in memory where the data for our string starts
// at `_str` we have the length of the string
// at `_str` + 32 -> we have the string itself
// here we get the size of the string
let strSize := mload(_str)
// here we add 32 to that address, so that we have the address of the string itself
let strAddr := add(_str, 32)
// we then pass the address of the string, and its size. This will hash our string
let hash := keccak256(strAddr, strSize)
// we store the hash value at slot 0 in memory
// just like we explained before, this is used as temporary storage (scratch space)
// no need to get the free memory pointer, it is faster (and cheaper) to use `0`
mstore(0, hash)
// we return what is stored at slot 0 (our hash) and the length of the hash (32)
return (0, 32)
}
}
// this is the same as `hashSecretWord1` but using a different technique
// here we use specific opcodes to manipulate calldata instead of using the parameters of the function
// instead of returning the hash, we'll assign it to storage variable `secretWord`
function hashSecretWord2(string calldata) external {
assembly {
// the calldata represents the entire data passed to a contract when calling a function
// the first 4 bytes always represent the signature of the function, and the rest are the parameters
// here we can skip the signature because we are already in the function, so the signature obviously represent the current function
// we can use CALLDATALOAD to load 32 bytes from the calldata.
// we use calldataload(4) to skip the signature bytes. This will therefore load the 1st parameter
// when using non-value types (array, mapping, bytes, string) the first parameter is going to be the offset where the parameter starts
// at that offset, we'll find the length of the parameter, and then the value
// this is the offset in `calldata` where our string starts
// here we use calldataload(4) -> loads the offset where the string starts
// -> we add 4 to that offset to take into account the signature bytes
// https://www.evm.codes/#35
let strOffset := add(4, calldataload(4))
// we use calldataload() again with the offset we just computed, this gives us the length of the string (the value stored at the offset)
let strSize := calldataload(strOffset)
// we load the free memory pointer
let ptr := mload(0x40)
// we copy the value of our string into that free memory
// CALLDATACOPY https://www.evm.codes/#37
// the string starts at the next memory slot, so we add 0x20 to it
calldatacopy(ptr, add(strOffset, 0x20), strSize)
// then we compute the hash of that string
// remember, the string is now stored at `ptr`
let hash := keccak256(ptr, strSize)
// and we store it to storage
sstore(secretWord.slot, hash)
}
}
function addMultipleGuesses(address[] memory _users, uint[] memory _guesses) external {
assembly {
// remember: `_users` is the address in memory where the parameter starts
// This is where the size of the array is stored. And then 32 bytes after, we have the values of the array
// so here we load what's at address `_users` -> which is the size of the array `_users`
let usersSize := mload(_users)
// same for `_guesses`
let guessesSize := mload(_guesses)
// we check that both arrays are the same size
// eq() returns 1 if they are equal, 0 if not equal
// we use iszero(). If they are not equal, we revert
// ISZERO https://www.evm.codes/#15
// EQ https://www.evm.codes/#14
// iszero(eq(...)) is the equivalent in assembly to !eq(...)
// REVERT https://www.evm.codes/#fd
if iszero(eq(usersSize, guessesSize)) { revert(0, 0) }
// we use a for-loop to loop through the items
for { let i := 0 } lt(i, usersSize) { i := add(i, 1) } {
// to get the ith value from the array we multiply i by 32 (0x20) and add it to `_users`
// we always have to add 1 to i first, because remember that `_users` is the size of the array, the values start 32 bytes after
// we could also do it this way (maybe it makes more sense):
// let userAddress := mload(add(add(_users, 0x20), mul(0x20, i)))
let userAddress := mload(add(_users, mul(0x20, add(i, 1))))
let userBalance := mload(add(_guesses, mul(0x20, add(i, 1))))
// we use the 0 memory slot as temporary storage to compute our hash
// we store the address there
mstore(0, userAddress)
// then the slot number for `guesses`
mstore(0x20, guesses.slot)
// we compute the slot number
let slot := keccak256(0, 0x40)
// and add our value to it
sstore(slot, userBalance)
}
}
}
}
contract AngleExplainsBase {
uint private secretNumber;
mapping(address => uint) public guesses;
bytes32 public secretWord;
// obviously this doesn't make sense
// but it will be fun to write it in assembly :D
function getSecretNumber() external view returns(uint) {
return secretNumber;
}
// this can only be set by an admin
// no access control because we want to keep it simple in assembly
function setSecretNumber(uint number) external {
secretNumber = number;
}
// a user can add a guess
function addGuess(uint _guess) external {
guesses[msg.sender] = _guess;
}
// yes I know... it doesn't make sense because you can change guesses for any user
// it's just to teach you how to parse arrays in assembly
function addMultipleGuesses(address[] memory _users, uint[] memory _guesses) external {
for (uint i = 0; i < _users.length; i++) {
guesses[_users[i]] = _guesses[i];
}
}
// this is useless since the `secretWord` is not used anywhere
// but this will teach us how to hash a string in assembly. Really cool! :)
function hashSecretWord(string memory _str) external {
secretWord = keccak256(abi.encodePacked(_str));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment