Cómo usar L1SLOAD, la clave para el Keystore y más

Ahmed Castro - Jun 12 - - Dev Community

Account Abstraction a través de múltiples chains será posible gracias al Keystore. Los usuarios podrán controlar múltiples smart contract wallet, en múltiples chains, con una sola cuenta. Este es un paso significativo para la tan esperada buena experiencia de usuario para los usuarios en un Ethereum centrado en rollups.

Para que esto sea posible, necesitamos poder leer los datos de L1 desde los rollups de L2, lo cual es actualmente un proceso muy costoso. Es por eso que Scroll recientemente introdujo el precompile L1SLOAD que puede leer el estado de L1 de manera rápida y económica. Safe Wallet ya está creando un concepto de prueba presentado en Safecon Berlín 2024, y creo que esto es solo el comienzo: DeFi, juegos, redes sociales y muchos más tipos de aplicaciones cross-chain serán posibles con esto.

Ahora aprendamos, con ejemplos prácticos, los fundamentos de esta nueva primitiva creada para interactuar con Ethereum de una nueva manera.

1. Conecta tu wallet al Devnet

Actualmente, L1SLOAD únicamente está disponible en Scroll Devnet. Porfavor no lo confundas con Scroll Sepolia Testnet. Son cadenas diferentes a pesar que ambas están encima de Sepolia.

Iniciemos conectando nuestra wallet al 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

2. Obten fondos en Scroll Sepolia Devnet

Existen dos métodos para obtener fondos, elige el que prefieras.

El faucet de telegram (recomendado)

Únete a este grupo de telegram y escribe /drop TUADRESS (e.g. /drop 0xd8da6bf26964af9d7eed9e03e53415d37aa96045) para recibir fondos.

Bridge de Sepolia

Puedes transferir ETH de Sepolia desde la Testnet de Sepolia a la Devnet de Scroll Sepolia a través del Scroll Messenger. Hay diferentes maneras de lograr esto, pero en este caso vamos a usar Remix.

Empecemos conectando tu wallet con Sepolia ETH a la Testnet de Sepolia. Recuerda que puedes obtener ETH de Sepolia gratis desde un faucet.

Ahora compila la interfaz a continuación.

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

interface ScrollMessanger {
    function sendMessage(address to, uint value, bytes memory message, uint gasLimit) external payable;
}
Enter fullscreen mode Exit fullscreen mode

A continuación, en la pestaña Deploy & Run, conecta la siguiente dirección de contrato: 0x9810147b43D7Fa7B9a480c8867906391744071b3.

Connect Scroll Messenger Interface on Remix

Ahora puedes enviar ETH llamando a la función sendMessage. Como se explica a continuación:

  • to: La dirección de tu wallet EOA. El destinatario de ETH en L2
  • value: La cantidad que deseas recibir en L2 en wei. Por ejemplo, si quieres enviar 0.01 ETH, debes pasar 10000000000000000
  • message: Déjalo vacío, solo pasa 0x00
  • gasLimit: 1000000 debería ser suficiente

También recuerda agregar algo de valor a tu transacción y agregar un poco de ETH extra para pagar el gas en L2, 0.001 debería ser más que suficiente. Así que, por ejemplo, si enviaste 0.01 ETH en el puente, envía una transacción con 0.011 ETH para cubrir las tarifas.

Send ETH from Sepolia to Scroll Devnet

Haz clic en el botón de transacción y tus fondos deberían estar disponibles en unos 15 minutos.


2. Despliega un contrato en L1

Como se mencionó anteriormente, L1SLOAD lee el estado del contrato L1 desde L2. Vamos a desplegar un simple contrato L1 con una variable number y luego acceder a ella desde L2.

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.20;

/**
 * @title Storage
 * @dev Store & retrieve value in a variable
 */
contract L1Storage {

    uint256 public number;

    /**
     * @dev Store value in variable
     * @param num value to store
     */
    function store(uint256 num) public {
        number = num;
    }

    /**
     * @dev Return value
     * @return value of 'number'
     */
    function retrieve() public view returns (uint256){
        return number;
    }
}
Enter fullscreen mode Exit fullscreen mode

Ahora llama a la función store(uint256 num) y pasa un nuevo valor. Por ejemplo, pasemos 42.

Store a value on L1

3. Recuperar un Slot desde L2

Ahora despleguemos el siguiente contrato en L2 pasando la dirección del contrato L1 que acabamos de desplegar como parámetro del constructor.

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

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

contract L2Storage {
    address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
    address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
    uint256 constant NUMBER_SLOT = 0;
    address immutable l1StorageAddr;

    constructor(address _l1Storage) {
        l1StorageAddr = _l1Storage;
    }

    function latestL1BlockNumber() public view returns (uint256) {
        uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
        return l1BlockNum;
    }

    function retrieveFromL1() public view returns(uint) {
        bytes memory input = abi.encodePacked(l1StorageAddr, NUMBER_SLOT);
        bool success;
        bytes memory ret;
        (success, ret) = L1_SLOAD_ADDRESS.staticcall(input);
        if (!success) {
            revert("L1SLOAD failed");
        }
        return abi.decode(ret, (uint256));
    }
}
Enter fullscreen mode Exit fullscreen mode

Nota que este contrato primero llama a latestL1BlockNumber() para obtener el último bloque de L1 que L2 tiene visibilidad. Y luego llama a L1SLOAD (opcode 0x101) pasando la dirección del contrato L1 y el slot 0 donde se almacena el uint number.

Ahora podemos llamar a retrieveFromL1() para obtener el valor que almacenamos previamente.

L2SLOAD L1 State red from L2

Ejemplo #2: Leyendo otros tipos de variables

Solidity almacena los slots en el mismo orden en que fueron declarados, esto es muy conveniente para nosotros. Por ejemplo, en el siguiente contrato account se almacenará en el slot #0, number en el slot #1 y text en el slot #2.

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

contract AdvancedL1Storage {
    address public account = msg.sender;
    uint public number = 42;
    string public str = "Hello world!";
}
Enter fullscreen mode Exit fullscreen mode

En el siguiente ejemplo se muestra cómo puedes consultar los diferentes slots y decodificarlos según su tipo. El único tipo nativo diferente que necesita una decodificación especial es el tipo string.

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.20;

contract L2Storage {
    address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
    address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
    address immutable l1ContractAddress;

    constructor(address _l1ContractAddress) {
        l1ContractAddress = _l1ContractAddress;
    }

    // Internal functions

    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);
    }

    // Public functions

    function retrieveAll() public view returns(address, uint, string memory) {
        bool success;
        bytes memory data;
        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");
        }

        address l1Account;
        uint l1Number;
        bytes32 l1Str;

        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));
    }
}
Enter fullscreen mode Exit fullscreen mode

Ejemplo #3: Leyendo un balance de un token ERC20 desde L1

Iniciemos lanzando un contrato muy sencillo de un token ERC20.

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

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

contract SimpleToken is ERC20 {
    constructor() ERC20("Simple Token", "STKN") {
        _mint(msg.sender, 21_000_000 ether);
    }
}
Enter fullscreen mode Exit fullscreen mode

Luego, podemos lanzar el siguiente contrato en L2 pasando en el costructor el address del token.

// SPDX-License-Identifier: GPL-3.0

pragma solidity ^0.8.20;

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

contract L2Storage {
    address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
    address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
    address immutable l1TokenAddress;

    constructor(address _l1TokenAddress) {
        l1TokenAddress = _l1TokenAddress;
    }

    // Internal functions

    function retrieveSlotFromL1(address l1StorageAddress, uint slot) internal view returns (bytes memory) {
        bool success;
        bytes memory returnValue;
        (success, returnValue) = L1_SLOAD_ADDRESS.staticcall(abi.encodePacked(l1StorageAddress, slot));
        if(!success)
        {
            revert("L1SLOAD failed");
        }
        return returnValue;
    }

    // Public functions
    function retrieveL1Balance(address account) public view returns(uint) {
        uint slotNumber = 0;
        return abi.decode(retrieveSlotFromL1(
            l1TokenAddress,
            uint(keccak256(
                abi.encodePacked(uint(uint160(account)),slotNumber)
                )
            )
            ), (uint));
    }
}
Enter fullscreen mode Exit fullscreen mode

Los contratos de OpenZeppelin colocan convenientemente el mapping de balances en el Slot 0. Así que puedes llamar a retrieveL1Balance() pasando la dirección de la cuenta como parámetro y el balance de tokens se almacenará en la variable l1Balance. Como puedes ver en el código, funciona convirtiendo la cuenta a uint160 y luego hashándola con el slot del mapeo, que es 0. Esto se debe a que así es como Solidity implementa los mappings en el fondo.

¡Gracias por leer esta guía!

Sígueme en dev.to y en Youtube para todo lo relacionado al desarrollo en Blockchain en Español.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player