Understanding Solidity Assembly: Using `shr` and `shl` for Byte Manipulation
This article will show some sample code for how to use the “shift right” and “shift left” (`shr` and `shl`, respectively) opcodes in Solidity’s assembly language in the context of manipulating bytes.
Our example case will be extracting a function’s arguments from a contract’s calldata. This functionality is both common and practical; in fact, the source code we will look at is a core piece of Optimism’s Canonical Transaction Chain contract.
The calldata that we will be using for reference looks like this(full data available on chain):
0xd0f893440000301d6c00007100000a0000060000000061fb647a0000d79f660000030000000061fb647a0000d79f680000080000000061fb65a60000d79f680000050000010061fb65a60000d79f690000040000010061fb65a60000d79f6b00000c0000000061fb65a60000d79f6c0000070000000061fb65a60000d79f6e0000170000000061fb65a60000d79f6f0000280000000061fb65a60000d79f720000030000000061fb65a60000d79f7300016ef9016b82010a830f424083e4e1c09482ac2ce43e33683c58be4cdc40975e73aa50f45980b90104b6b1b6c30000000000000000000000007161c3416e08abaa5cd38e68d9a28e43a694e037...
Using `shr`
The first thing you will notice is that shouldStartAtElement
uses shr
with an integer and a calldataload
function passed into it.
calldataload
accepts a location in the calldata as an argument (4) and returns the subsequent 32 bytes. So we have this section selected:
Which yields this bytes32 object:
To interpret a data type smaller than 32 bytes (like uint40, which is 5 bytes) the compiler simply reads from the right:
Without shifting, the operation would yield a return value equaling the integer value of the hex object 0x0000030000
, or 196608.
The integer argument that shr
accepts is the number of bits to shift the 32 byte block to the right by. Because there are 8 bits in a byte, it equals a 27 byte shift.
But because the object still needs to be 32 total bytes, the side that it is shifting from will be padded with zeroes. The uint40 interpretation of shr(216, calldataload(4))
would then be:
0x00000301d6c
(which you will recognize as the first 10 bytes of the bytes32 object loaded from calldataload(4)
) or, as an integer, 3153260.
Using shl
is the same thing, except the bytes32 object gets shifted left in the window instead of right.
Verifying Results
Applying the same calculations to the rest of the variables yields the following results:
shouldStartAtElement = 3153260
totalElementsToAppend = 113
numContexts = 10
These values are verifiable using the Optimism block explorer. Batch 23718 indeed consists of 113 elements and links to the L1 block in question. Its first transaction is exactly one higher than the shouldStartAtElement
, which means it is the next to be recorded.