← Back to list

L1SLOAD Simple Guide: Read the L1 State from L2

Account abstraction has been in place since the EIP-4337 was authorised and accepted via governance in 2023. The Keystore will enable…

West · 2024-11-29 23:42 · 60 claps · 5.8 min read
#ethereum #blockchain #l1 #l2 #crosscahin
Open on Medium ↗
Wiki topics: TLS · Design Tools & Workflow CRY · Crypto & Web3 LIT · Literature & Writing

L1SLOAD Simple Guide: Read the L1 State from L2

Account abstraction has been in place since the EIP-4337 was authorised and accepted via governance in 2023. The Keystore will enable seamless cross-chain account abstraction features, which would improve the user experience for the Ethereum community and Rollups.

For this to happen we need to be able to read the L1 data from L2 rollups which is currently an expensive process. Because of this, Scroll introduced the L1SLOAD precompile that can read the L1 state fast and cheaply. Projects like Safe Wallet are already creating a proof concept introduced at Safecon Berlin 2024 of this work. More and more cross-chain applications will be possible with this.

We will go through the basics of L1SLOAD which introduces new ways to interact with Ethereum.

Connect Your Wallet to The Devnet

L1SLOAD is only available on the Scroll Devnet which you should not confuse with the Scroll Sepolia Testnet. Both of them are deployed on top of Sepolia Testnet but they are separate chains.

Start by connecting your wallet to Scroll Devnet

Name: Scroll Devnet RPC: https://l1sload-rpc.scroll.io Chain ID: 2227728 Symbol: Sepolia ETH Explorer: https://l1sload-blockscout.scroll.io

Connect to Scroll Devnet

Connect to Scroll Devnet

Get Some Devnet Funds

I recommend you use the Telegram faucet bot by starting the bot and typing /drop YOURADDRESS

Deploy a Contract on L1

L1SLOAD reads L1 contract state on L2. Now let’s deploy a simple L1 contract with a number variable and later access it from L2

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.20;

// Dummy contract we'll deploy on L1 and then read the state from L2
contract L1Storage {

    // This is the first variable declared on this contract so it will be stored at the slot 0
    uint256 public number;

    // Stores a variable
    function store(uint256 num) public {
        number = num;
    }

    // Returns the number stored, keep in mind we won't call this function from L2 since we'll read the slot directly
    function retrieve() public view returns (uint256){
        return number;
    }

}

Now call the store(uint256 num function and pass a new value. For example, let’s pass 21.

Store a value on L1

Store a value on L1

Retrieve a Slot from L2

Now deploy the following contract on L2 by passing the L1 contract address you just deployed as constructor param.

// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;

interface IL1Blocks {
function latestBlockNumber() external view returns (uint256);
}

contract L2Storage {
// This precompile returns the latest block accesible by L2, it is not mandatory to use this precompile but it can help to keep track of the L2 progress
address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
// This is the L1SLOAD precompile address
address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
// The number varaiable is stored at the slot 0
uint256 constant NUMBER_SLOT = 0;
address immutable l1StorageAddr;

    // The constructor receives the contract we just deployed on L1
    constructor(address _l1Storage) {
        l1StorageAddr = _l1Storage;
    }

    // Again, this function is for reference only. It returns the latest L1 block number red by L2
    function latestL1BlockNumber() public view returns (uint256) {
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        return l1BlockNum;
    }

    // Returns the number read from L1
    function retrieveFromL1() public view returns(uint) {
        // The precompile expects the contract address number and an array of slots. In this case we only query one, the slot 0
        bytes memory input = abi.encodePacked(l1StorageAddr, NUMBER_SLOT);
        bool success;
        bytes memory ret;
        // We can access any piece of state of L1 through a staticcall, this makes it simple and cheap
        (success, ret) = L1_SLOAD_ADDRESS.staticcall(input);
        if (!success) {
            revert("L1SLOAD failed");
        }
        return abi.decode(ret, (uint256));
    }

}

Now you can call retrieveFromL1() to get the value you previously stored.

Example: Reading Other Variable Types

Solidity stores the slots in the same order as they were declared. For example in the contract below account will be stored on slot #0, number slot #1 and text on slot #2.

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

// This time we will query multiple slots with diverse data types
contract AdvancedL1Storage {
address public account = msg.sender;
uint public number = 42;
string public str = "Hello world!";
}

In the following example you can notice how you can query the different slots and decode accordingly to address, uint256, etc. The only different native type that needs special decoding is the string type.

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.20;

// This contract queries multiple slots in one call
contract L2Storage {
address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
address immutable l1ContractAddress;

    constructor(address _l1ContractAddress) {
        l1ContractAddress = _l1ContractAddress;
    }

    // String will need to be decoded to be returned as a typical solidity string
    function bytes32ToString(bytes32 _bytes32) public pure returns (string memory) {
        bytes memory bytesArray = new bytes(32);
        for (uint256 i; i < 32; i++) {
            if(_bytes32[i] == 0x00)
                break;
            bytesArray[i] = _bytes32[i];
        }
        return string(bytesArray);
    }

    // In a single function, many slots can be retrieved
    function retrieveAll() public view returns(address, uint, string memory) {
        bool success;
        bytes memory data;
        // This time we will query slot 0 (account), slot 1 (number) and slot 2 (str)
        uint[] memory l1Slots = new uint[](3);
        l1Slots[0] = 0;
        l1Slots[1] = 1;
        l1Slots[2] = 2;
        (success, data) = L1_SLOAD_ADDRESS.staticcall(abi.encodePacked(l1ContractAddress, l1Slots));
        if(!success)
        {
            revert("L1SLOAD failed");
        }

        // We will store them in typical solidity variables
        address l1Account;
        uint l1Number;
        bytes32 l1Str;

        // In order to read types with a size different than 32 bytes we will need a little bit of assembly
        // But fear not! The code is not as difficult as it sounds
        assembly {
            let temp := 0x20
            // Load the data into memory
            let ptr := add(data, 32) // Start at the beginning of data skipping the length field

            // Store the first slot from L1 into the account variable
            mstore(temp, mload(ptr))
            l1Account := mload(temp)
            ptr := add(ptr, 32)

            // Store the second slot from L1 into the number variable
            mstore(temp, mload(ptr))
            l1Number := mload(temp)
            ptr := add(ptr, 32)

            // Store the third slot from L1 into the str variable
            mstore(temp, mload(ptr))
            l1Str := mload(temp)
        }
        return (l1Account, l1Number, bytes32ToString(l1Str));
    }

}

Example: Reading ERC20 Token Balance from L1

First, you should deploy this very simple ERC20 token

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

// In our final example, we'll read the balance of any ERC20 holder on L1
// Please note that we will be using OpenZeppelin's implementation which puts the balance mapping on slot 0, is is not enforced by the ERC20 standard
contract SimpleToken is ERC20 {
constructor() ERC20("Simple Token", "STKN") {
\_mint(msg.sender, 21_000_000 ether);
}
}

Next, you deploy the contract on L2 by passing the L1 token address as parameter.

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.20;

interface IL1Blocks {
function latestBlockNumber() external view returns (uint256);
}

// This contract reads the balance of any holder on L1
contract L2Storage {
address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
address immutable l1TokenAddress;

    constructor(address _l1TokenAddress) {
        l1TokenAddress = _l1TokenAddress;
    }

    // Retrieves the token balance of a given Ethereum account.
    function retrieveL1Balance(address account) public view returns(uint) {
        // We assume that the balance mapping is stored at slot number 0
        uint slotNumber = 0;
        // The formula that Solidity uses to compute the slot in mappings is: balanceSlotPosition = keccak256(holderAddress, slotNumber)
        uint accountBalanceSlot = uint(
            keccak256(abi.encodePacked(uint(uint160(account)),
            slotNumber)
        ));
        // Now, we perform a staticcall to the l1sload precompile to retrieve the account balance
        bool success;
        bytes memory returnValue;
        (success, returnValue) = L1_SLOAD_ADDRESS.staticcall(abi.encodePacked(l1TokenAddress, accountBalanceSlot));
        if(!success)
        {
            revert("L1SLOAD failed");
        }
        // The retrieved value is in bytes32 format, so we cast it to uint256 before returning it
        return abi.decode(returnValue, (uint));
    }

}

You can call the retrieveL1Balance by passing the account address as a parameter and the token balance will be returned as OpenZeppelin contracts conveniently places the balances mapping on slot 0. as you can see from the code, it works by converting the account to uint160 and then hashing it with the mapping slot which is 0. It’s because that’s the way solidity implements mapping.

Account abstraction and cross-chain applications integration is a huge phase of scaling Ethereum and Rollups and I believe that with L1LSLOAD Scroll has taken it a bit further to help read L1 state from L2. The l1sload precompile is currently under public scrutiny before its testnet deployment. Share your thoughts on the Ethereum Magicians Forum to help shape its development through community feedback.

Thanks for reading and I hope you use your skills to develop and improve Ethereum.

Resources

Got everything for this article from this beautifully written L1SLOAD guide by Filosofia Codigo: https://www.levelup.xyz/content/l1sload-guide-read-the-l1-state-from-l2

Give Filosofia a follow on X: https://x.com/FilosofiaCodigo


메타데이터
post_id
1df935e9cc0e
slug
l1sload-simple-guide-read-the-l1-state-from-l2-1df935e9cc0e
url
https://medium.com/@west_XE/l1sload-simple-guide-read-the-l1-state-from-l2-1df935e9cc0e
canonical_url
https://medium.com/@west_XE/l1sload-simple-guide-read-the-l1-state-from-l2-1df935e9cc0e
author_url
https://medium.com/@west_XE
status
ok
fetched_at
2026-06-27 07:40:21