Back

L1SLOAD guide: Reading Data Structures

Written by FilosofiaCodigo

Sep 04, 2024 · 15 min read

This article is a continuation of the L1SLOAD guide where we introduced its basic usage. Now, we will explore how to use l1sload to read more complex storage structures, such as mappings, structs, and both static and dynamic arrays.

The EVM handles the state by using 32-byte storage slots. In Solidity, this is abstracted to provide a better developer experience. However, to be read complex data structures with l1sload, it's essential to understand how Solidity manages storage at a lower level.

So let's start by understanding how static arrays are managed by the EVM.

Static Arrays

Static arrays are those with a fixed length declared at compile time. For example:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract L1ArrayDemo { uint[5] myArray; constructor() { myArray[0] = 10; myArray[1] = 20; myArray[2] = 30; myArray[3] = 40; myArray[4] = 50; } }

On-chain, the first element of the array is stored in the variable’s storage slot, the second in slot + 1, the third in slot + 2, and so on.

Static Array Storage Demo

L1ArrayDemo storage layout

To retrieve an element, you can simply add the array's slot to the array index of the value you're looking for. The formula looks like this:

Formula Static Arrays

Keep in mind, the length of a static array is not stored on-chain. It's only visible at compile time.

Another important note is that this approach works for types larger than 16 bytes (e.g., uint256, address, uint200). For smaller types like bool or uint8, Solidity applies data optimizations, which we’ll cover later in this guide.

The following contract demonstrates how to read an array from the L1ArrayDemo contract.

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract L2ArrayDemo { address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001; address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101; function getNum(address contractAddress, uint arraySlot, uint arrayIndex) public view returns(uint) { bool success; bytes memory returnValue; (success, returnValue) = L1_SLOAD_ADDRESS.staticcall(abi.encodePacked(contractAddress, arraySlot + arrayIndex)); if(!success) { revert("L1SLOAD failed"); } return abi.decode(returnValue, (uint)); } }

Dynamic Arrays

Unlike static arrays, dynamic arrays can grow after deployment. Because of this, Solidity stores them differently on-chain.

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract L1DynamicArrayDemo { uint[] public myArray; constructor() { myArray.push(10); myArray.push(20); myArray.push(30); } }

The storage slot for a dynamic array holds its length. The actual data is stored starting at KECCAK256(ARRAY_SLOT), with elements stored sequentially after that position, just like static arrays.

Dynamic Array Storage Demo

L1DynamicArrayDemo storage layout

To query a specific element, we use the following formula:

Formula Dynamic Arrays

Here’s an example contract that queries both the length and specific elements of a dynamic array:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract L2DynamicArrayDemo { address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001; address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101; function retrieveArrayElement(address contractAddress, uint arraySlot, uint arrayIndex) public view returns(uint) { require(arrayIndex < getArrayLength(contractAddress, arraySlot), "Out of bounds index"); uint arrayElementSlot = uint( keccak256( abi.encodePacked(arraySlot) ) ) + arrayIndex; bool success; bytes memory returnValue; (success, returnValue) = L1_SLOAD_ADDRESS.staticcall(abi.encodePacked(contractAddress, arrayElementSlot)); if(!success) { revert("L1SLOAD failed"); } return abi.decode(returnValue, (uint)); } function getArrayLength(address contractAddress, uint arraySlot) public view returns(uint) { bool success; bytes memory returnValue; (success, returnValue) = L1_SLOAD_ADDRESS.staticcall(abi.encodePacked(contractAddress, arraySlot)); if(!success) { revert("L1SLOAD failed"); } return abi.decode(returnValue, (uint)); } }

Special Case: Storing Values 16 Bytes or Smaller

Consider a dynamic array of uint104 values. Since two uint104 values can fit into a single 32-byte storage slot (with 48 bits, or 6 bytes, remaining unused), Solidity packs them together.

// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract L1Uint104Array { uint104[] public myArray; constructor() { myArray.push(10); myArray.push(20); myArray.push(30); myArray.push(40); myArray.push(50); } }
Uint104 Array Storage Demo

L1Uint104Array storage layout

To query such values using l1sload, we first locate the appropriate slot, then use bitwise operations to extract the desired value. The following formula helps in calculating the slot:

Formula Dynamic Arrays with other sizes getting the slot

However, retrieving the value also requires shifting the bits appropriately, which can be done using the right-shift operator.

Shifting bytes to get value once we have the slot

This method can also work for larger types like uint256 or address, but for simplicity and efficiency, I recommend sticking to the methods mentioned earlier for those types.

Here’s an example contract that queries data stored in a uint104 array:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract L2Uint104ArrayDemo { address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001; address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101; function retrieveArrayElement(address contractAddress, uint arraySlot, uint arrayIndex) public view returns(uint104) { require(arrayIndex < getArrayLength(contractAddress, arraySlot), "Out of bounds index"); uint arrayElementSlot = uint( keccak256( abi.encodePacked(arraySlot) ) ) + arrayIndex / (uint(256)/104); bool success; bytes memory returnValue; (success, returnValue) = L1_SLOAD_ADDRESS.staticcall(abi.encodePacked(contractAddress, arrayElementSlot)); if(!success) { revert("L1SLOAD failed"); } uint104 returnValueUint = uint104(abi.decode(returnValue, (uint)) >> ((arrayIndex % (uint(256)/104)) * 104)); return returnValueUint; } function getArrayLength(address contractAddress, uint arraySlot) public view returns(uint) { bool success; bytes memory returnValue; (success, returnValue) = L1_SLOAD_ADDRESS.staticcall(abi.encodePacked(contractAddress, arraySlot)); if(!success) { revert("L1SLOAD failed"); } return abi.decode(returnValue, (uint)); } }

Structs

Structs store their data contiguously, similar to arrays. If multiple elements can fit into a single 32-byte slot, Solidity will pack them together to optimize storage.

// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.20; struct MyStruct { uint a;// 32 bytes (256 bits) address b; // 20 bytes (160 bits) bool c; // 1 byte (8 bits) uint d; // 32 bytes (256 bits) } contract L1StructDemo { MyStruct myStruct; constructor() { myStruct = MyStruct(10,address(this),true,20); } }

In the following example, the struct fields a, b, and c are packed across multiple slots. The variable d doesn’t fit into the second slot, so it is stored in the next available slot.

Struct Storage Demo

L1StructDemo storage layout

Reading structs requires accounting for the byte offsets of each field, using bitwise operations as necessary.

// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.20; struct MyStruct { uint a;// 32 bytes (256 bits) address b; // 20 bytes (160 bits) bool c; // 1 byte (8 bits) uint d; // 32 bytes (256 bits) } // This contract reads the balance of any holder on L1 contract L2StructDemo { address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001; address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101; function getArrayLength(address contractAddress, uint structSlot) public view returns(MyStruct memory) { bool success; bytes memory returnValue; (success, returnValue) = L1_SLOAD_ADDRESS.staticcall(abi.encodePacked(contractAddress, structSlot, structSlot+1, structSlot+2)); if(!success) { revert("L1SLOAD failed"); } (uint256 slot0, uint256 slot1, uint slot2) = abi.decode(returnValue, (uint, uint, uint)); address b = address(uint160(slot1)); bool c = (slot1 >> 160) == 1; return MyStruct(slot0, b, c, slot2); } }

Nesting Structures

Nested structures, such as mappings of arrays or structs containing arrays, follow the same rules as their underlying structures.

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract L1NestingDemo { mapping(uint => uint[] myArray) arrayMapping; constructor() { arrayMapping[0].push(10); arrayMapping[0].push(20); arrayMapping[0].push(30); arrayMapping[1].push(100); arrayMapping[1].push(200); arrayMapping[1].push(300); } }

For example, reading from a mapping of arrays requires combining the formulas for mappings and dynamic arrays. The formula for mappings from the previous article, where introduced Mappings, works like this:

Mapping forumula

The dynamic array formula we introduced in this article is the following:

Dynamic Array formula

We combine them and we get this one.

Array Mapping formula

Data is stored in a pattern as illustrated below:

Nesting Storage Demo

L1NestingDemo storage layout

Take a look at this example that demonstrates reading a mapping of arrays:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract L2NestingDemo { address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001; address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101; function getArrayLength(address contractAddress, uint mappingSlot, uint mappingKey, uint arrayIndex) public view returns(uint) { uint mappingArraySlot = uint( keccak256( abi.encodePacked( keccak256(abi.encodePacked(mappingKey, mappingSlot) ) ) )) + arrayIndex; bool success; bytes memory returnValue; (success, returnValue) = L1_SLOAD_ADDRESS.staticcall(abi.encodePacked(contractAddress, mappingArraySlot)); if(!success) { revert("L1SLOAD failed"); } return abi.decode(returnValue, (uint)); } }

You can extend these principles to more complex data structures, such as an array of mappings, structs with arrays, or even mappings of mappings. The same logic applies in all cases.

Next steps

To improve your understanding of Solidity storage layout, check out the official documentation on the Layout of State section. Additionally, share your thoughts on the Ethereum Magicians Forum regarding the L1SLOAD RIP. With l1sload currently in development, your input is very important at this stage.

Thanks for reading!

© 2024 Scroll Foundation | All rights reserved

Terms of UsePrivacy Policy