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:

One byte is 2 characters, so the length as a string would be 64.

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.

Visualizing a shift of 3 bytes

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.

Subscribe to Alex Otsu
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.