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
2. Obten fondos en Scroll Sepolia Devnet
Existen dos métodos para obtener fondos, elige el que prefieras.
Únete a este grupo de telegram y escribe El faucet de telegram (recomendado)
/drop TUADRESS
(e.g. /drop 0xd8da6bf26964af9d7eed9e03e53415d37aa96045
) para recibir fondos.
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. A continuación, en la pestaña Deploy & Run, conecta la siguiente dirección de contrato: Ahora puedes enviar ETH llamando a la función También recuerda agregar algo de valor a tu transacción y agregar un poco de ETH extra para pagar el gas en L2, Haz clic en el botón de transacción y tus fondos deberían estar disponibles en unos 15 minutos.Bridge de Sepolia
// 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;
}
0x9810147b43D7Fa7B9a480c8867906391744071b3
.sendMessage
. Como se explica a continuación:
0.01
ETH, debes pasar 10000000000000000
0x00
1000000
debería ser suficiente0.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.
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;
}
}
Ahora llama a la función store(uint256 num)
y pasa un nuevo valor. Por ejemplo, pasemos 42
.
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));
}
}
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.
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!";
}
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));
}
}
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);
}
}
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));
}
}
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.
Top comments (0)