Dry running Solidity-Yul code
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 with 0001F4
// 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)