Recentemente escrevi um artigo sobre Karma Money , um sistema monetário alternativo baseado em um token ERC-20 exclusivo. Imaginei o Karma Money como um sistema fechado, onde os usuários também podem pagar taxas de transação com o karma. Construir nosso próprio blockchain seria uma forma de tornar isso realidade, mas é uma tarefa desafiadora. Para garantir a segurança e a credibilidade da blockchain, precisaríamos estabelecer a infraestrutura necessária e uma comunidade suficientemente grande. Seria muito mais simples usar um blockchain existente. Existem redes como Gnosis ou Polygon , que são totalmente compatíveis com Ethereum e têm taxas de transação muito baixas. A taxa para uma transação ERC20 nessas redes é normalmente inferior a 1 centavo. O problema é que essa taxa deve ser paga na própria criptomoeda da rede, o que pode dificultar o uso da rede pelos usuários. Felizmente, existe uma solução intermediária, as transações de metal EIP-712 .
No caso de uma metatransação, o usuário cria uma estrutura que descreve uma transação e a assina digitalmente com sua chave privada. É semelhante a preencher um cheque para alguém. A transação assinada digitalmente é então enviada para um nó de retransmissão, que a submete a um contrato inteligente. O contrato verifica a assinatura e, se for válido, executa a transação. O nó retransmissor paga pela execução do contrato.
Em uma transação de carma, por exemplo, o usuário fornece o valor da transação (por exemplo, 10 dólares de carma), o endereço Ethereum para onde deseja enviar o valor e uma taxa de transação (em dólares de carma) que está disposto a oferecer. para a transação. Essa estrutura é assinada digitalmente e enviada para um nó retransmissor. Se o nó considerar a taxa de transação aceitável, ele submete a estrutura assinada digitalmente ao contrato de carma, que verifica a assinatura e executa a transação. Como a taxa de transação é paga na moeda nativa do blockchain pelo nó de retransmissão, parece ao usuário que ele está pagando com dólares de carma pela transação sem precisar de seu próprio blockchain.
Depois da teoria, vamos dar uma olhada na prática.
O padrão EIP-712 define como assinar pacotes de dados estruturados de maneira padronizada. MetaMask exibe esses dados estruturados em um formato legível para o usuário. Uma estrutura compatível com EIP-712, conforme mostrado no MetaMask ( pode ser testado nesta URL ) se parece com isto:
A transação acima foi gerada usando o seguinte código simples:
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()
O eip712domain_type_definition é uma descrição de uma estrutura geral, que contém os metadados. O campo name é o nome da estrutura, o campo version é a versão de definição da estrutura e os campos chainId e verifyingContract determinam a qual contrato a mensagem se destina. O contrato em execução verifica esses metadados para garantir que a transação assinada seja executada apenas no contrato de destino.
O karma_request_domain contém o valor específico dos metadados definidos pela estrutura EIP712Domain.
A estrutura real que enviamos ao MetaMask para assinatura está contida na variável transfer_request . O bloco de tipos contém as definições de tipo. Aqui, o primeiro elemento é a definição obrigatória de EIP712Domain, que descreve os metadados. Isto é seguido pela definição da estrutura real, que neste caso é TransferRequest. Esta é a estrutura que aparecerá no MetaMask para o usuário. O bloco de domínio contém o valor específico dos metadados, enquanto a mensagem contém a estrutura específica que queremos assinar com o usuário.
Quando se trata de dinheiro de carma, um exemplo de como uma metatransação é montada e enviada para o contrato inteligente é assim:
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))
A variável types define a estrutura da transação. O “de” é o endereço do remetente, enquanto o “para” é o endereço do destinatário. O valor representa a quantidade de tokens a serem transferidos. A taxa é a “quantidade” de tokens que oferecemos ao nó de retransmissão em troca da execução de nossa transação e cobertura do custo na moeda nativa da cadeia. O “nonce” serve como contador para garantir a exclusividade da transação. Sem este campo, uma transação poderia ser executada várias vezes. No entanto, graças ao nonce, uma transação assinada só pode ser executada uma vez.
A função signTypedData fornecida por ethers.js facilita a assinatura de estruturas EIP-712. Ele faz a mesma coisa que o código apresentado anteriormente, mas com um uso mais simples.
A metaTransfer é o método do contrato de carma para executar a metatransação. Vamos ver como isso 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 validar a assinatura, devemos primeiro gerar o hash da estrutura. As etapas exatas para fazer isso são descritas em detalhes no padrão EIP-712 , que inclui um exemplo de contrato inteligente e um exemplo de código javascript .
Em resumo, a essência é combinarmos o TYPEHASH (que é o hash da descrição da estrutura) com os campos da estrutura usando abi.encode. Em seguida, produz um hash keccak256. O hash é passado para o método _hashTypedDataV4, herdado do contrato EIP712 OpenZeppelin no contrato Karma. Esta função adiciona metadados à nossa estrutura e gera o hash final, tornando a validação da estrutura muito simples e transparente. A função mais externa é ECDSA.tryRecover, que tenta recuperar o endereço do signatário a partir do hash e da assinatura. Se corresponder ao endereço do parâmetro “from”, a assinatura é válida. No final do código, a transação real é executada e o nó retransmissor que realiza a transação recebe a taxa.
EIP-712 é um padrão geral para estruturas de assinatura, tornando-o apenas um dos muitos usos para implementação de metatransações. Como a assinatura pode ser validada não apenas com contratos inteligentes, também pode ser muito útil em aplicações não-blockchain. Por exemplo, pode ser usado para autenticação do lado do servidor, onde o usuário se identifica com sua chave privada. Tal sistema pode fornecer um alto nível de segurança normalmente associado a criptomoedas, permitindo a possibilidade de utilizar uma aplicação web apenas com uma chave de hardware. Além disso, chamadas de API individuais também podem ser assinadas com a ajuda do MetaMask.
Espero que esta breve visão geral do padrão EIP-712 tenha sido inspiradora para muitos e que você possa utilizá-lo em projetos baseados em blockchain e não-blockchain.
Cada código está disponível no repositório GitHub de karma money .