Las meta transacciones son una manera popular de permitir a los usuarios realizar transacciones en una blockchain sin pagar directamente los costos de gas. En lugar de eso, firman un mensaje fuera de la cadena, que luego es retransmitido por un retransmisor que paga las tarifas de gas.
Diagrama de Interacción
Los actores de este esquema son:
- User (Usuario): firma una meta transacción (que es un mensaje que contiene información sobre la transacción que le gustaría ejecutar).
- Relayer (Retransmisor): un servidor web con una cartera que firma una transacción válida de Ethereum (que tiene la meta transacción como carga útil) y la envía a la blockchain.
- Forwarder (Redireccionador): un contrato de Ethereum encargado de verificar la firma de la meta transacción que, como es de esperarse, reenvía la solicitud a un contrato receptor.
- Recipient (Receptor): el contrato de Ethereum que el usuario pretendía llamar sin pagar la tarifa de gas, este contrato tiene que ser capaz de preservar la identidad del usuario que solicitó inicialmente la transacción.
El contrato Redireccionador
El mecanismo básico está dado por la implementación de MinimalForwarder de OpenZeppelin, para ser usado junto con un contrato compatible con ERC2771 como el contrato Recipient
.
Contrato Receptor
Usaremos el siguiente contrato Recipient
para nuestro proyecto de muestra, que es una implementación del contrato ERC2771.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/metatx/ERC2771Context.sol";
contract Recipient is ERC2771Context {
event FlagCaptured(address previousHolder, address currentHolder, string color);
address public currentHolder = address(0);
string public color = "white";
constructor(address trustedForwarder) ERC2771Context(trustedForwarder) {}
function setFlagOwner(string memory _color) external {
address previousHolder = currentHolder;
currentHolder = _msgSender();
color = _color;
emit FlagCaptured(previousHolder, currentHolder, color);
}
function getFlagOwner() external view returns (address, string memory) {
return (currentHolder, color);
}
}
EIP-712: Datos estructurados tipados JSON
Para poder firmar la solicitud de meta transacción, se debe crear un JSON de datos estructurados y debería verse así:
{
"types": {
"EIP712Domain": [
{"name": "name", "type": "string"},
{"name": "version", "type": "string"},
{"name": "chainId", "type": "uint256"},
{"name": "verifyingContract", "type": "address"}
],
"ForwardRequest": [
{"name": "from", "type": "address"},
{"name": "to", "type": "address"},
{"name": "value", "type": "uint256"},
{"name": "gas", "type": "uint256"},
{"name": "nonce", "type": "uint256"},
{"name": "data", "type": "bytes"}
]
},
"primaryType": "ForwardRequest",
"domain": {
"name": "MinimalForwarder",
"version": "0.0.1",
"chainId": <network_chain_id>,
"verifyingContract": "<forwarder_contract_address>"
},
"message": {
"from": "<user_address>",
"to": "<recipient_contract_address>",
"value": 0,
"gas": 210000,
"nonce": "",
"data": ""
}
}
Los datos JSON anteriores especifican el separador de dominio EIP712 y los tipos de mensajes para una meta transacción utilizando la estructura ForwardRequest
, que tiene los siguientes campos:
-
from
(dirección): la dirección del remitente -
to
(dirección): la dirección del contrato receptor -
value
(uint256): la cantidad de Ether a enviar (0 en nuestro caso) -
gas
(uint256): el límite de gas para la transacción -
nonce
(uint256): el nonce del remitente -
data
(bytes): la llamada a la función del contrato receptor codificada en ABI
Implementación con Web3j
Crear una instancia Web3j y conectarla a un nodo:
Web3j web3j = Web3j.build(new HttpService("<network_http_rpc_endpoint>"));
Los contratos inteligentes MinimalForwarder
y Recipient
se pueden compilar usando web3j-sokt.se.
Se pueden generar envoltorios (wrappers) Java de contratos inteligentes Solidity usando web3j-maven-plugin o web3j-gradle-plugin. Estas clases Java se usarán en los pasos restantes.
Cargar el contrato MinimalForwarder
y el contrato Recipient
usando las respectivas direcciones de contrato, asumiendo que ya has desplegado el contrato MinimalForwarder
y el contrato receptor.
MinimalForwarder minimalForwarder = MinimalForwarder.load("<minimalForwarder_contract_address>", web3j, credentials, new StaticGasProvider(BigInteger.valueOf(4_100_000_000L),BigInteger.valueOf(6_721_975L)));
Recipient recipient = Recipient.load("<recipient_contract_address>", web3j, credentials, new StaticGasProvider(BigInteger.valueOf(4_100_000_000L),BigInteger.valueOf(6_721_975L)));
Definir la función del receptor y codificarla:
final Function recipientFunction = new Function(
"setFlagOwner",
List.of(new org.web3j.abi.datatypes.Utf8String("blue")),
Collections.emptyList()) {
};
String encodedFunction = FunctionEncoder.encode(recipientFunction);
Obtener el nonce actual para el contrato MinimalForwarder
:
BigInteger nonce = minimalForwarder.getNonce(credentials.getAddress()).send();
Crear un objeto ForwardRequest
:
MinimalForwarder.ForwardRequest forwardRequest = new MinimalForwarder.ForwardRequest(
credentials.getAddress(),
recipient.getContractAddress(),
BigInteger.ZERO,
BigInteger.valueOf(210000),
nonce,
Numeric.hexStringToByteArray(encodedFunction));
Ahora lee el JSON de datos estructurados tipados EIP712, agregue los valores vacíos y firme usando Sign.signTypedData()
:
String jsonMessageString = Files.readString(Paths.get("src/main/resources/data.json").toAbsolutePath());
JSONObject jsonObject = new JSONObject(jsonMessageString);
jsonObject.getJSONObject("message").put("from", credentials.getAddress());
jsonObject.getJSONObject("message").put("nonce", nonce);
jsonObject.getJSONObject("message").put("data", encodedFunction);
String modifiedJsonString = jsonObject.toString();
Sign.SignatureData signature = Sign.signTypedData(modifiedJsonString, credentials.getEcKeyPair());
Obtener la firma en Bytes:
byte[] retval = new byte[65];
System.arraycopy(signature.getR(), 0, retval, 0, 32);
System.arraycopy(signature.getS(), 0, retval, 32, 32);
System.arraycopy(signature.getV(), 0, retval, 64, 1);
Ahora ejecuta la meta transacción y verifica si la salida es correcta:
minimalForwarder.execute(forwardRequest, getSignatureBytes(retval, BigInteger.valueOf(210000)).send();
System.out.println(recipient.color().send()); // devuelve color "azul"
Si ves la salida a recipient.color().send()
, te dará “azul”, que fue el parámetro pasado a través de la meta transacción y por lo tanto, confirma que todo está funcionando como se esperaba.
Las meta transacciones ofrecen una manera conveniente para que los usuarios interactúen con una blockchain sin preocuparse por las tarifas de gas. Al utilizar el Web3j y los contratos de ejemplo proporcionados, los desarrolladores pueden probar la funcionalidad de meta transacciones e implementarlas en sus propios dApps.
Si estás interesado en implementar y experimentar con meta transacciones usando Web3j, te recomendamos profundizar en el tutorial proporcionado y explorar los contratos de ejemplo. También familiarízate con la documentación de Web3j para entender cómo interactuar con contratos inteligentes de Ethereum usando Java.
Referencias
- https://docs.web3j.io/4.10.0/use_cases/meta_transaction/
- https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/MinimalForwarder.sol
- https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/metatx/ERC2771Context.sol
- https://github.com/donpabblo/meta-transaction/blob/main/contracts/Recipient.sol
- https://medium.com/coinmonks/gas-free-transactions-meta-transactions-explained-f829509a462d
Artículo original publicado por Nischal Sharma. Traducción de Paulinho Giovannini.
Discussion (0)