WEB3DEV

Cover image for Como Assinar Dados Estruturados EIP-712 com a MetaMask
Adriano P. Araujo
Adriano P. Araujo

Posted on

Como Assinar Dados Estruturados EIP-712 com a MetaMask

A MetaMask é uma das carteiras criptográficas mais utilizadas, mas oferece muito mais do que isso. Com sua ajuda, temos a possibilidade de assinar digitalmente estruturas de dados, que podem ser utilizadas de diversas maneiras. Uma opção é usar a MetaMask para autenticar nossos usuários. Nesse caso, identificamos nossos usuários com um endereço Ethereum em vez de uma senha. O usuário comprova a propriedade do endereço Ethereum assinando digitalmente uma estrutura de dados, e a assinatura é então validada no lado do servidor. Como a MetaMask armazena de forma segura nossa chave privada e até oferece a opção de usar carteiras de hardware, esse tipo de autenticação é muito mais seguro do que soluções tradicionais baseadas em senha.

Existe também a opção de assinar determinadas chamadas de API com a MetaMask. Por exemplo, no caso de um serviço financeiro, além da autenticação da MetaMask, podemos assinar transações financeiras individuais separadamente, tornando-as muito mais seguras.

As assinaturas da MetaMask podem ser validadas na blockchain com a ajuda de um contrato inteligente. Uma área de uso para isso são as metatransações. Com as metatransações, podemos simplificar o uso de aplicativos blockchain para os usuários. No caso das metatransações, as taxas de transação são pagas por um servidor de retransmissão em vez do usuário, para que o usuário não precise ter criptomoedas. O usuário simplesmente monta a transação, a assina usando a MetaMask e a envia para o servidor de retransmissão, que a encaminha para um contrato inteligente. O contrato inteligente valida a assinatura digital e executa a transação.

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. A MetaMask exibe esses dados estruturados de forma legível para o usuário. Uma estrutura compatível com a EIP-712, como mostrado na MetaMask (pode ser testada neste URL), se parece com isso:

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()
Enter fullscreen mode Exit fullscreen mode

A 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 para qual contrato a mensagem é destinada. 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 para a MetaMask assinar está contida na variável transfer_request. O bloco types contém as definições de tipo. Aqui, o primeiro elemento é a definição obrigatória do EIP712Domain, que descreve os metadados. Isso é seguido pela definição real da estrutura, que neste caso é a TransferRequest. Esta é a estrutura que aparecerá na MetaMask para o usuário. O bloco domain contém o valor específico dos metadados, enquanto a mensagem contém a estrutura específica que desejamos assinar com o usuário.

A assinatura pode ser facilmente validada no lado do servidor usando o eth-sig-util:

import { recoverTypedSignature } from '@metamask/eth-sig-util'  

const address = recoverTypedSignature({  
    data: typedData,  
    signature: signature,  
    version: SignTypedDataVersion.V4  
}))
Enter fullscreen mode Exit fullscreen mode

A função recoverTypedSignature tem três parâmetros. O primeiro parâmetro é a estrutura de dados, o segundo é a assinatura, e o último é a versão da assinatura. O valor de retorno da função é o endereço Ethereum recuperado.

Agora, vamos ver como a assinatura pode ser validada na cadeia por um contrato inteligente. O código é do meu repositório karma money (você pode ler mais sobre karma money aqui). O seguinte código TypeScript envia uma metatransação para o contrato inteligente karma money:

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))
Enter fullscreen mode Exit fullscreen mode

A variável types define a estrutura da transação. O "from" é o endereço do remetente, enquanto o "to" é o endereço do destinatário. A quantidade 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 um contador para garantir a singularidade da transação. Sem esse campo, uma transação poderia ser executada várias vezes. No entanto, graças ao nonce, uma transação assinada pode ser executada apenas uma vez.

A função signTypedData fornecida pelo ethers.js facilita a assinatura de estruturas EIP-712. Ela faz a mesma coisa que o código apresentado anteriormente, mas com um uso mais simples.

O metaTransfer é o método do contrato karma para executar uma 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;
    }
Enter fullscreen mode Exit fullscreen mode

Para validar a assinatura, é necessário primeiro gerar o hash da estrutura. Os passos exatos para fazer isso são detalhados no padrão EIP-712, que inclui um contrato inteligente de exemplo e um código JavaScript de exemplo.

Em resumo, a essência é que combinamos o TYPEHASH (que é o hash da descrição da estrutura) com os campos da estrutura usando o abi.encode. Isso produz um hash keccak256. O hash é passado para o método _hashTypedDataV4, herdado do contrato EIP712 da OpenZeppelin no contrato Karma. Essa 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 coincidir com o endereço do parâmetro "from", a assinatura é válida. No final do código, a transação real é executada, e o nó de retransmissão que realiza a transação recebe a taxa.

A EIP-712 é um padrão geral para assinar estruturas, tornando-o apenas um de muitos usos para implementar 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 relacionadas a blockchain. Por exemplo, pode ser usado para autenticação no lado do servidor, onde o usuário se identifica com sua chave privada. Esse sistema pode fornecer um alto nível de segurança normalmente associado a criptomoedas, permitindo a possibilidade de usar um aplicativo da web apenas com uma chave de hardware. Além disso, chamadas individuais de API também podem ser assinadas com a ajuda da MetaMask.

Espero que esta breve visão geral do padrão EIP-712 tenha sido inspiradora para muitos e que você consiga utilizá-la tanto em projetos baseados em blockchain quanto em projetos não relacionados a blockchain.


Este artigo foi escrito por Laszlo Fazekas e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.

Latest comments (0)