Recientemente escribí un artículo sobre Karma Money , un sistema de moneda alternativo basado en un token ERC-20 único. Imaginé a Karma Money como un sistema cerrado, donde los usuarios también pueden pagar tarifas de transacción con karma. Construir nuestra propia cadena de bloques sería una forma de hacerlo realidad, pero es una tarea desafiante. Para garantizar la seguridad y credibilidad de blockchain, necesitaríamos establecer la infraestructura necesaria y una comunidad suficientemente grande. Sería mucho más sencillo utilizar una cadena de bloques existente. Existen cadenas como Gnosis o Polygon , que son totalmente compatibles con Ethereum y tienen tarifas de transacción muy bajas. La tarifa por una transacción ERC20 en estas cadenas suele ser inferior a 1 centavo. El problema es que esta tarifa debe pagarse en la propia criptomoneda de la cadena, lo que puede complicar el uso de la cadena a los usuarios. Afortunadamente, existe una solución puente, las transacciones de metales EIP-712 .
En el caso de una metatransacción, el usuario crea una estructura que describe una transacción y luego la firma digitalmente con su clave privada. Es similar a escribir un cheque para alguien. Luego, la transacción firmada digitalmente se envía a un nodo de retransmisión, que la somete a un contrato inteligente. El contrato verifica la firma y, si es válida, ejecuta la transacción. El nodo de retransmisión paga por la ejecución del contrato.
En una transacción de karma, por ejemplo, el usuario proporciona el monto de la transacción (por ejemplo, 10 dólares de karma), la dirección de Ethereum a la que desea enviar el monto y una tarifa de transacción (en dólares de karma) que está dispuesto a ofrecer. para la transacción. Esta estructura se firma digitalmente y se envía a un nodo de retransmisión. Si el nodo considera aceptable la tarifa de transacción, envía la estructura firmada digitalmente al contrato de karma, que verifica la firma y ejecuta la transacción. Dado que el nodo de retransmisión paga la tarifa de transacción en la moneda nativa de la cadena de bloques, al usuario le parece como si estuviera pagando con dólares de karma por la transacción sin necesitar su propia cadena de bloques.
Después de la teoría, echemos un vistazo a la práctica.
El estándar EIP-712 define cómo firmar paquetes de datos estructurados de forma estandarizada. MetaMask muestra estos datos estructurados en un formato legible para el usuario. Una estructura compatible con EIP-712, como se muestra en MetaMask ( se puede probar en esta URL ) se ve así:
La transacción anterior se generó utilizando el siguiente código simple:
async function main() { if (!window.ethereum || !window.ethereum.isMetaMask) { console.log("Please install MetaMask") return } const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' }); const chainId = await window.ethereum.request({ method: 'eth_chainId' }); const eip712domain_type_definition = { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ] } const karma_request_domain = { "name": "Karma Request", "version": "1", "chainId": chainId, "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" } document.getElementById('transfer_request')?.addEventListener("click", async function () { const transfer_request = { "types": { ...eip712domain_type_definition, "TransferRequest": [ { "name": "to", "type": "address" }, { "name": "amount", "type": "uint256" } ] }, "primaryType": "TransferRequest", "domain": karma_request_domain, "message": { "to": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", "amount": 1234 } } let signature = await window.ethereum.request({ "method": "eth_signTypedData_v4", "params": [ accounts[0], transfer_request ] }) alert("Signature: " + signature) }) } main()
eip712domain_type_definition es una descripción de una estructura general, que contiene los metadatos. El campo de nombre es el nombre de la estructura, el campo de versión es la versión de definición de la estructura y los campos chainId y verificaringContract determinan a qué contrato está destinado el mensaje. El contrato en ejecución verifica estos metadatos para garantizar que la transacción firmada solo se ejecute en el contrato de destino.
karma_request_domain contiene el valor específico de los metadatos definidos por la estructura EIP712Domain.
La estructura real que enviamos a MetaMask para su firma está contenida en la variable transfer_request . El bloque de tipos contiene las definiciones de tipos. Aquí, el primer elemento es la definición obligatoria EIP712Domain, que describe los metadatos. A esto le sigue la definición de estructura real, que en este caso es TransferRequest. Esta es la estructura que aparecerá en MetaMask para el usuario. El bloque de dominio contiene el valor específico de los metadatos, mientras que el mensaje contiene la estructura específica que queremos firmar con el usuario.
Cuando se trata de dinero karma, un ejemplo de cómo se arma una metatransacción y se envía al contrato inteligente es el siguiente:
const types = { "TransferRequest": [ { "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "amount", "type": "uint256" }, { "name": "fee", "type": "uint256" }, { "name": "nonce", "type": "uint256" } ] } let nonce = await contract.connect(MINER).getNonce(ALICE.address) const message = { "from": ALICE.address, "to": JOHN.address, "amount": 10, "fee": 1, "nonce": nonce } const signature = await ALICE.signTypedData(karma_request_domain, types, message) await contract.connect(MINER).metaTransfer(ALICE.address, JOHN.address, 10, 1, nonce, signature) assert.equal(await contract.balanceOf(ALICE.address), ethers.toBigInt(11))
La variable tipos define la estructura de la transacción. El "de" es la dirección del remitente, mientras que el "a" es la dirección del destinatario. La cantidad representa la cantidad de tokens que se transferirán. La tarifa es la “cantidad” de tokens que ofrecemos al nodo de retransmisión a cambio de ejecutar nuestra transacción y cubrir el costo en la moneda nativa de la cadena. El “nonce” sirve como contador para asegurar la unicidad de la transacción. Sin este campo, una transacción podría ejecutarse varias veces. Sin embargo, gracias al nonce, una transacción firmada sólo se puede ejecutar una vez.
La función signTypedData proporcionada por ethers.js facilita la firma de estructuras EIP-712. Hace lo mismo que el código presentado anteriormente pero con un uso más sencillo.
La metaTransferencia es el método del contrato de karma para ejecutar la metatransacción. Vamos a ver cómo funciona:
function metaTransfer( address from, address to, uint256 amount, uint256 fee, uint256 nonce, bytes calldata signature ) public virtual returns (bool) { uint256 currentNonce = _useNonce(from, nonce); (address recoveredAddress, ECDSA.RecoverError err) = ECDSA.tryRecover( _hashTypedDataV4( keccak256( abi.encode( TRANSFER_REQUEST_TYPEHASH, from, to, amount, fee, currentNonce ) ) ), signature ); require( err == ECDSA.RecoverError.NoError && recoveredAddress == from, "Signature error" ); _transfer(recoveredAddress, to, amount); _transfer(recoveredAddress, msg.sender, fee); return true; }
Para poder validar la firma primero debemos generar el hash de la estructura. Los pasos exactos para hacer esto se describen en detalle en el estándar EIP-712 , que incluye un contrato inteligente de muestra y un código javascript de muestra .
En resumen, la esencia es que combinamos el TYPEHASH (que es el hash de la descripción de la estructura) con los campos de la estructura usando abi.encode. Luego produce un hash keccak256. El hash se pasa al método _hashTypedDataV4, heredado del contrato EIP712 OpenZeppelin en el contrato Karma. Esta función agrega metadatos a nuestra estructura y genera el hash final, haciendo que la validación de la estructura sea muy simple y transparente. La función más externa es ECDSA.tryRecover, que intenta recuperar la dirección del firmante a partir del hash y la firma. Si coincide con la dirección del parámetro “de”, la firma es válida. Al final del código, se ejecuta la transacción real y el nodo de retransmisión que realiza la transacción recibe la tarifa.
EIP-712 es un estándar general para firmar estructuras, lo que lo convierte en solo uno de los muchos usos para implementar metatransacciones. Como la firma se puede validar no sólo con contratos inteligentes, también puede resultar muy útil en aplicaciones que no son blockchain. Por ejemplo, se puede utilizar para la autenticación del lado del servidor, donde el usuario se identifica con su clave privada. Un sistema de este tipo puede proporcionar un alto nivel de seguridad típicamente asociado con las criptomonedas, permitiendo la posibilidad de utilizar una aplicación web sólo con una clave de hardware. Además, también se pueden firmar llamadas API individuales con la ayuda de MetaMask.
Espero que esta breve descripción general del estándar EIP-712 haya sido inspiradora para muchos y que pueda utilizarlo tanto en proyectos basados en blockchain como sin blockchain.
Cada código está disponible en el repositorio de GitHub de karma money .