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 o , 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 .
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.
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.
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 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; }
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.