Las funciones de abstracción de cuentas cross-chain serán posibles gracias a los Keystores. Los usuarios podrán controlar varias smart contracts wallets, en múltiples chains, con una sola llave. Esto puede traer la tan esperada buena experiencia de usuario para los usuarios finales en los rollups de Ethereum..
In order to make this happen, we need to be able to read the L1 data from L2 rollups which is currently a very expensive process. That’s why Scroll recently introduced the L1SLOAD
precompile that is able to read the L1 State fast and cheap. Safe wallet is already creating a proof of concept introduced at Safecon Berlin 2024 of this work and I think this is just the begining: DeFi, gamming, social and many more types of corss-chain applications are possible with this.
Para que esto suceda, necesitamos poder leer los datos de L1 desde los rollups en L2, lo cual actualmente es un proceso muy costoso. Es por eso que Scroll introdujo recientemente el precompile L1SLOAD
que es capaz de leer el estado de L1 de manera rápida y económica. Safe wallet ha creado un demo presentado en Safecon Berlín 2024. Pienso que esto es solo el comienzo, esto podrá mejorar aplicaciones cross-chain en DeFi, juegos, redes sociales y muchos más.
Vamos ahora a aprender, con ejemplos prácticos, los conceptos básicos de esta nueva primitiva que está abre la puerta a una nueva forma de interactuar con Ethereum.
1. Conecta tu wallet al Scroll Devnet
Actualmente, L1SLOAD
está disponible únicamente en la Scroll Devnet. Toma nota y no la confundas con la Scroll Sepolia Testnet. Aunque ambos están desplegados sobre el Sepolia Testnet, son cadenas separadas.
Comenzamos conectando nuestra wallet a la Scroll Devnet:
- Name:
Scroll Devnet
- RPC:
https://l1sload-rpc.scroll.io
- Chain id:
222222
- Symbol:
Sepolia ETH
- Explorer:
https://l1sload-blockscout.scroll.io
2. Obtener fondos en Scroll Devnet
Existen dos métodos de obtener fondos en la Scroll Devnet. Elige el que prefieras.
Bot de Faucet en Telegram (recomendado)
Únete a este grupo de telegram y escribe /drop TUADDRESS
(e.g. /drop 0xd8da6bf26964af9d7eed9e03e53415d37aa96045
) para recibir fondos directo a tu cuenta.
Bridge de Sepolia
Puedes enviar fondos de Sepolia a la Scroll Devnet a través del bridge. Existen dos maneras de lograr esto pero en este caso usaremos Remix.
Conectemos ahora tu wallet con Sepolia ETH a Sepolia Testnet. Recuerda que puedes obtener Sepolia ETH grátis en un faucet,
Ahora compila la siguiente interfaz.
// 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;
}
A continuación, en la tab de “Deploy & Run” conecta el contrato siguiente: 0x561894a813b7632B9880BB127199750e93aD2cd5
.
Ahora puedes enviar ETH llamando la función sendMessage
como se detalla a continación:
- to: La dirección de tu cuenta EOA. El address que recibirá fondos en L2.
- value: La cantidad de ether que deseas recibir en L2, en formato wei. Por ejemplo, si envías
0.01
ETH debes pasar como parámetro10000000000000000
- message: Déjalo en blanco, simplemente envía
0x00
- gasLimit:
1000000
debería ser suficiente
Also remember to pass some value to your transaction. And add some extra ETH to pay for fees on L2, 0.001
should be more than enough. So if for example you sent 0.01
ETH on the bridge, send a transaction with 0.011
ETH to cover the fees.
También recuerda pasar un extra value
en tu transacción. Es decir, agrega un poco de ETH extra para pagar las comisiones en L2, 0.001
debería ser más que suficiente. Así que, por ejemplo, si enviaste 0.01 ETH en el bridge, envía una transacción con 0.011
ETH para cubrir las comisiones.
Haz click en el botón transact
y tus fondos deberían llegar en 15 mins aproximadamente.
2. Lanza tu contrato en L2
Tal y como mencionamos anteriormente, L1SLOAD
lee el estado de contratos en L1 desde L2. Lancemos ahora un contrato simple en L1 que luego leeremos el valor de la variable number
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;
}
}
Now call the store(uint256 num)
function and pass a new value. For example let’s pass 42
.
3. Obtener una slot desde L2
Lanzamos el siguiente contrato en L2 pasando el address del contrato que recién lanzamos en L1 como parámetro en el 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;
uint public l1Number;
constructor(address _l1Storage) {
l1StorageAddr = _l1Storage;
}
function latestL1BlockNumber() public view returns (uint256) {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
return l1BlockNum;
}
function retrieveFromL1() public {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
bytes memory input = abi.encodePacked(l1BlockNum, l1StorageAddr, NUMBER_SLOT);
bool success;
bytes memory ret;
(success, ret) = L1_SLOAD_ADDRESS.call(input);
if (success) {
(l1Number) = abi.decode(ret, (uint256));
} else {
revert("L1SLOAD failed");
}
}
}
Observa que este contrato primero llama latestL1BlockNumber()
para obtener el más reciente bloque en L1 que está disponible para lectura en L2. Luego, llamamos L1SLOAD
(opcode 0x101
) pasando el address del contrato en L1 como parámetro y la slot 9 que es donde la variable number
está ubicada dentro de ese contrato.
Ahora podemos llamar retrieveFromL1()
para obtener el valor almacenado previamente.
Ejemplo #2: Leer otros tipos de variables
Solidity guarda las slots de las variables en el mismo orden en la que fueron declaradas. Esto es bastante conveniente para nosotros. Por ejemplo, en el siguiente contrato, account
se almacena en la slot #0, number
en la #1 y text
en la #2.
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
contract AdvancedL1Storage {
address public account;
uint public number;
string public text;
}
Podemos observar que cómo podemos obtener los valores de diferentes tipos: uint256, address, etc… Las strings son un poco diferente por la naturaleza variable de su tamaño.
// 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 l1ContractAddress;
address public account;
uint public number;
string public test;
constructor(address _l1ContractAddress) { //0x5555158Ea3aB5537Aa0012AdB93B055584355aF3
l1ContractAddress = _l1ContractAddress;
}
// Internal functions
function latestL1BlockNumber() internal view returns (uint256) {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
return l1BlockNum;
}
function retrieveSlotFromL1(uint blockNumber, address l1StorageAddress, uint slot) internal returns (bytes memory) {
bool success;
bytes memory returnValue;
(success, returnValue) = L1_SLOAD_ADDRESS.call(abi.encodePacked(blockNumber, l1StorageAddress, slot));
if(!success)
{
revert("L1SLOAD failed");
}
return returnValue;
}
function decodeStringSlot(bytes memory encodedString) internal pure returns (string memory) {
uint length = 0;
while (length < encodedString.length && encodedString[length] != 0x00) {
length++;
}
bytes memory data = new bytes(length);
for (uint i = 0; i < length; i++) {
data[i] = encodedString[i];
}
return string(data);
}
// Public functions
function retrieveAddress() public {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
account = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 0), (address));
}
function retrieveNumber() public {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
number = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 1), (uint));
}
function retrieveString() public {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
test = decodeStringSlot(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 2));
}
function retrieveAll() public {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
account = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 0), (address));
number = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 1), (uint));
test = decodeStringSlot(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 2));
}
}
Ejemplo #3: Leer el balance de un token ERC20 en L1
Comenzamos lanzando un token ERC20 bastante sencillo.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract SimpleToken is ERC20 {
constructor(
string memory name,
string memory symbol,
uint256 initialSupply
) ERC20(name, symbol) {
_mint(msg.sender, initialSupply * 1 ether);
}
}
A continuación, lanzamos el siguiente contrato en L2 pasando como parámetros el address del token que recién lanzamos en L1.
// 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 l1ContractAddress;
uint public l1Balance;
constructor(address _l1ContractAddress) {
l1ContractAddress = _l1ContractAddress;
}
// Internal functions
function latestL1BlockNumber() public view returns (uint256) {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
return l1BlockNum;
}
function retrieveSlotFromL1(uint blockNumber, address l1StorageAddress, uint slot) internal returns (bytes memory) {
bool success;
bytes memory returnValue;
(success, returnValue) = L1_SLOAD_ADDRESS.call(abi.encodePacked(blockNumber, l1StorageAddress, slot));
if(!success)
{
revert("L1SLOAD failed");
}
return returnValue;
}
// Public functions
function retrieveL1Balance(address account) public {
uint slotNumber = 0;
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
l1Balance = abi.decode(retrieveSlotFromL1(
l1BlockNum,
l1ContractAddress,
uint(keccak256(
abi.encodePacked(uint160(account),slotNumber)
)
)
), (uint));
}
}
Los contratos de OpenZeppelin colocan convenientemente el mapping de los balances del token en el Slot 0. Así que puedes llamar a retrieveL1Balance()
pasando el address del holder como parámetro y el balance del token se almacenará en la variable l1Balance
. Como puedes ver en el código, el proceso es primer convertir el address a uint160 y luego lo hasheamos con el slot del mapping, que es 0. Esto se debe a que es así como Solidity implementa los mappings.
¡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.
Source link
lol