July 12th, 2023
In my attempt to understand Solidity at a deeper level, I wanted to understand how bytes are manipulated using Yul during read and write functions. Thanks to Degatchi’s blog, I was able to see normal setter and getter functions implemented in Yul. However, the blog does not show how the bytes are changing as the code gets executed, so I wanted to dry-run the assembly code and show it.
The given function is an external function that is used to update a variable value. Normally the code is pretty straightforward, but here, we are using Yul where we can have more control over memory and reduce gas efficiently.
I am going to go through each line and show what exactly happens to the slot in the process.
Storage:
0x01 : 00000000000000 047b37ef4d76c2366f795fb557e3c15e0607b7d8 000014 000a
// unused bytes c b a // before: 00000000000000 047b37ef4d76c2366f795fb557e3c15e0607b7d8 000014 000a // unused bytes c b a // after: 00000000000000 047b37ef4d76c2366f795fb557e3c15e0607b7d8 0001F4 000a function set_b(uint24 b) external { assembly { // Removing the `uint16` from the right. // before: 00000000000000 047b37ef4d76c2366f795fb557e3c15e0607b7d8 000014 000a // ^ // after: 0000 00000000000000 047b37ef4d76c2366f795fb557e3c15e0607b7d8 000014 // ^ let new_v := shr(0x10, sload(0x01)) // Create our mask. new_v := and(0xffffff, new_v) // Input our value into the mask. new_v := xor(b, new_v) // Add back the removed `a` value bits. new_v := shl(0x10, new_v) // Replace original 32 bytes `000014` with `0001F4`. new_v := xor(new_v, sload(0x01)) // Store our new value. sstore(0x01, new_v) } }
Breaking down each line of the code:
- Get rid of
000a
because out input will begin from the right hand side and it will be easier to modify the value directly instead of padding the input.
// Removing the `uint16` from the right. let new_v = shr(0x10, sload(0x01)) /* * before: 00000000000000 047b37ef4d76c2366f795fb557e3c15e0607b7d8 000014 000a * * new_v: 0000 00000000000000 047b37ef4d76c2366f795fb557e3c15e0607b7d8 000014 */
- Now we have to create our mask which will only keep our specific area and remove everything else.
// Create our mask // This will only keep the 000014 new_v := and(0xffffff, new_v) /* * before: 0000 00000000000000 047b37ef4d76c2366f795fb557e3c15e0607b7d8 000014 * mask: 0000 00000000000000 0000000000000000000000000000000000000000 ffffff * * new_v: 0000 00000000000000 0000000000000000000000000000000000000000 000014 */
XOR gate
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
- Input our value into the mask. [TBA]
// Input our value into the mask. new_v := xor(b, new_v) /* * new_v: 0000 00000000000000 0000000000000000000000000000000000000000 000014 * b: 0000 00000000000000 0000000000000000000000000000000000000000 0001F4 * new_v: 0000 00000000000000 0000000000000000000000000000000000000000 0001e0 */
- Add back the removed `a` value bits.
new_v := shl(0x10, new_v) /* * new_v: 0000 00000000000000 0000000000000000000000000000000000000000 0001e0 * * after: 00000000000000 0000000000000000000000000000000000000000 0001e0 0000 */
- Replace original 32 bytes
000014
with0001F4
// Replace original 32 bytes `000014` with `0001F4`. new_v := xor(new_v, sload(0x01)) /* * before: 00000000000000 0000000000000000000000000000000000000000 0001e0 0000 * 0x01: 00000000000000 047b37ef4d76c2366f795fb557e3c15e0607b7d8 000014 000a * * after: 00000000000000 047b37ef4d76c2366f795fb557e3c15e0607b7d8 0001f4 000a */
Then, sstore just updates the bytes in storage
slot 1
to new_v
sstore(0x01, new_v)