WEB3DEV

Cover image for Como Assinar e Verificar Mensagens EIP712 Off-chain e On-chain
Fatima Lima
Fatima Lima

Posted on

Como Assinar e Verificar Mensagens EIP712 Off-chain e On-chain

Image description

Requisitos da EIP712

De acordo com a EIP712, para assinar cada mensagem, você precisa fornecer um Domain Separator (separador de domínio) específico para o dApp, para garantir sua exclusividade em comparação com contratos semelhantes. Isso envolve definir um nome e uma versão para o contrato, bem como o chainId implantado e o endereço do contrato de verificação. Você também pode fornecer um sal para aumentar a exclusividade. A EIP712 explica:

É possível que dois DApps apresentem uma estrutura idêntica, como Transfer(address from,address to,uint256 amount), que não deve ser compatível. Com a introdução de um separador de domínio, os desenvolvedores de dApp têm a garantia de que não haverá choque de assinaturas.

Os parâmetros a seguir são opcionais, mas aumentam a segurança de seu contrato contra ataques de repetição.

name: o nome do dApp ou do protocolo, ex. “Polytrade”.

version: a versão atual do que o padrão chama de "domínio de assinatura". Esse pode ser o número da versão de seu dApp ou plataforma. Ele impede que as assinaturas de uma versão do dApp funcionem com as de outras.

chainId: a identidade da cadeia EIP-155. Impede que uma assinatura destinada a uma rede, como uma rede de teste, funcione em outra, como a rede principal. A palavra-chave block.chainid no Solidity retorna a id da cadeia atual.

verifyingContract: o endereço Ethereum do contrato que verificará a assinatura resultante. O endereço da palavra-chave address(this) no Solidity retorna o endereço do proprietário do contrato, que pode ser usado na verificação da assinatura.

O Message Type para o separador de domínio é:

 const domainType = {
       "EIP712Domain": [
           {
             "name": "name",
             "type": "string"
           },
           {
             "name": "version",
             "type": "string"
           },
           {
             "name": "chainId",
             "type": "uint256"
           },
           {
             "name":"verifyingContract",
             "type": "address"
           }
       ]
   }
Enter fullscreen mode Exit fullscreen mode

Depois de definir o TypedMessage, enviaremos os valores na mesma estrutura:

   const domainData = {
       name: "Polytrade",
       version: "1.0",
       chainId: chainId,
       verifyingContract:
       verifyingContractAddress
   };
Enter fullscreen mode Exit fullscreen mode

A EIP2612, que é a extensão Permit para tokens ERC20, permite a aprovação por meio da assinatura de uma mensagem seguindo o padrão EIP712. Portanto, ela inclui o separador de domínio junto com uma estrutura Permit:

const permitType = {
     "Permit": [{
         "name": "owner",
         "type": "address"
       },
       {
         "name": "spender",
         "type": "address"
       },
       {
         "name": "value",
         "type": "uint256"
       },
       {
         "name": "nonce",
         "type": "uint256"
       },
       {
         "name": "deadline",
         "type": "uint256"
       }
     ]
   }
Enter fullscreen mode Exit fullscreen mode

Da mesma forma que o Domain Separator, depois de definir o TypedMessage, temos os valores para ele:

const permitData: {
       "owner": owner,
       "spender": spender,
       "value": value,
       "nonce": nonce,
       "deadline": deadline
   }
Enter fullscreen mode Exit fullscreen mode

Agora que temos todos os componentes necessários, podemos prosseguir com a assinatura da mensagem preparada e, em seguida, verificar se o endereço do proprietário é o signatário da mensagem.

1. Assinando Off-chain

a. Ethers _signTypedData

Como explicado nos docs, você pode usar este método para assinar uma mensagem usando a especificação EIP-712 e recuperar a assinatura.

> signer._signTypedData( domain , types , value )
Enter fullscreen mode Exit fullscreen mode
signature = await signer._signTypedData(
       domainData,
       permitType,
       permitData
   );
Enter fullscreen mode Exit fullscreen mode

b. Web3 eth_signTypedData_v4

De acordo com os Docs, ao enviar os dados com o signatário, deve-se usar um método V3 ou V4.

  • V1 é baseado em uma versão anterior da EIP-712 que não possuía algumas melhorias de segurança mais recentes e, em geral, deve ser desprezado em favor de versões posteriores.
  • V3 é baseado na EIP-712, exceto pelo fato de que não há suporte para matrizes e estruturas de dados recursivas.
  • V4 é baseado na EIP-712 e inclui suporte total a matrizes e estruturas de dados recursivas.
const data = JSON.stringify({
       types: {
           EIP712Domain: domainType,
           Permit: permitType,
       },
       domain: domainData,
       primaryType: "Permit",
       message: permitData
   });

   web3.currentProvider.send(
     {
       method: "eth_signTypedData_v4",
       params: [signer, data],
       from: signer
     },
     function(err, result) {
       if (err) {
         return console.error(err);
       }
     }
   )
Enter fullscreen mode Exit fullscreen mode

c. Metamask eth-sig-util signTypedData

Para a função mencionada, você pode especificar a versão e fornecer a chave privada do signatário junto com os dados.

> signTypedData(privateKey, data, version)
Enter fullscreen mode Exit fullscreen mode
ethSigUtil.signTypedData(privateKey, {
       data: {
         types: {
             EIP712Domain:domainTye,
             Permit: permitType
         },
         domain: domainData,
         primaryType: 'Permit',
         message: permitData
       },
       "V4"
   });
Enter fullscreen mode Exit fullscreen mode

Dividir a Assinatura

É possível enviar a assinatura junto com os parâmetros para um contrato inteligente para verificação da mensagem. No entanto, para a EIP2612, você precisa enviar os parâmetros de assinatura r, s, e v para a função permit. Portanto, precisamos dividir o hash para extrair esses parâmetros.

splitSignature() por ethers, pode fazer o trabalho:

const { r, s, v } = splitSignature(signature);
Enter fullscreen mode Exit fullscreen mode

Como alternativa, é possível dividir a assinatura em seus componentes da seguinte forma:

 const r = signature.slice(0, 66);
   const s = "0x" + signature.slice(66, 130);
   const v = parseInt(signature.slice(130, 132), 16);
Enter fullscreen mode Exit fullscreen mode

2. Verificando on-chain

A assinatura on-chain com o Solidity é simples. Primeiro, precisamos codificar o tipo de domínio EIP712 e, em seguida, podemos obter o hash keccak dele usando as seguintes etapas:

bytes32 internal constant _EIP_712_DOMAIN_TYPEHASH =
       keccak256(
           abi.encodePacked(
               "EIP712Domain(",
               "string name,",
               "string version,",
               "uint256 chainId,",
               "address verifyingContract",
               ")"
           )
       );
Enter fullscreen mode Exit fullscreen mode

Da mesma forma, para o Permit typehash, pode-se acompanhar o mesmo processo de codificar e obter o hash keccak:

 bytes32 internal constant _PERMIT_TYPEHASH =
       keccak256(
           abi.encodePacked(
               "Permit(",
               "address owner,",
               "address spender,",
               "uint256 value,",
               "uint256 nonce,",
               "uint256 deadline",
               ")"
           )
       );
Enter fullscreen mode Exit fullscreen mode

Para acomodar os valores do separador de domínio, ao passar o name e a version, e para recalcular dinamicamente o _DOMAIN_SEPARATOR, no caso de alterações no endereço do contrato ou no ID da cadeia, evitamos codificação rígida. Para calcular o separador de domínio, primeiro obtemos o hash do name e da version. Em seguida, fazemos o hash do tipo de domínio junto com todos os valores combinados.

_NAME_HASH = keccak256(bytes(name));
   _VERSION_HASH = keccak256(bytes(version));
   _DOMAIN_SEPARATOR = keccak256(
       abi.encode(
           _EIP_712_DOMAIN_TYPEHASH,
           _NAME_HASH,
           _VERSION_HASH,
           block.chainid,
           address(this)
       )
   );
Enter fullscreen mode Exit fullscreen mode

Para o PERMIT, seguiremos o mesmo processo:

bytes32 PERMIT = keccak256(
       abi.encode(
           _PERMIT_TYPEHASH,
           owner,
           spender,
           value,
           nonce,
           deadline
       )
   );
Enter fullscreen mode Exit fullscreen mode

Para obter o resumo (digest) da EIP712 para recuperação de endereço público, concatenamos os hashes de ambos _DOMAIN_SEPARATOR e PERMIT com o prefixo \x19\x01. Em seguida, fazemos o hash da concatenação resultante para obter o resumo necessário para a recuperação do endereço. O prefixo 0x19 é baseado na EIP191, que é usado para padronização de dados assinados, e 01 significa dados estruturados da EIP712.

> {0x1901}{_DOMAIN_SEPARATOR}{PERMIT}
Enter fullscreen mode Exit fullscreen mode

Esse código de montagem garante a execução segura obtendo primeiro o ponteiro de memória. Em seguida, ele grava o prefixo inicial de 2 bytes, seguido de _DOMAIN_SEPARATOR e PERMIT. Por fim, ele calcula o hash do resultado concatenado e retorna o resumo do resultado:

assembly {
           let ptr := mload(0x40)
           mstore(ptr, "\x19\x01")
           mstore(add(ptr, 0x02), _DOMAIN_SEPARATOR)
           mstore(add(ptr, 0x22), PERMIT)
           digest := keccak256(ptr, 0x42)
       }
Enter fullscreen mode Exit fullscreen mode

Aqui está a versão Solidity do código que executa as mesmas operações:

digest = keccak256(abi.encodePacked(uint16(0x1901), _DOMAIN_SEPARATOR, PERMIT));
Enter fullscreen mode Exit fullscreen mode

Agora que tudo está preparado, podemos obter o endereço público usando a função ecrecover(digest, v, r, s).

ecrecover(digest, v, r, s);
Enter fullscreen mode Exit fullscreen mode

Ao afirmar a equivalência dos endereços do signatário e do proprietário, podemos garantir que eles sejam idênticos, o que nos permite aumentar a quantidade permitida ao gastador.

bytes32 r;
       bytes32 s;
       uint8 v;
       if (signature.length == 64) {
           bytes32 vs;
           (r, vs) = abi.decode(signature, (bytes32, bytes32));
           s = vs & (0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
           v = uint8(uint256(vs >> 255)) + 27;
       } else if (signature.length == 65) {
           (r, s) = abi.decode(signature, (bytes32, bytes32));
           v = uint8(signature[64]);
       }
Enter fullscreen mode Exit fullscreen mode

Pontos-chave

  • Observe que, para fins de verificação off-chain, podemos utilizar as mesmas etapas para gerar hashes de domínio e de permissão (manualmente ou utilizando o assistente TypedDataUtils.hashStruct da biblioteca eth-sig-util). Em seguida, podemos criar um digest a partir desses hashes. Por fim, podemos usar a função recoverAddress(digest, signature) da biblioteca ethers ou a função recoverTypedSignature(data, signature, version) da biblioteca eth-sig-util para recuperar o endereço do signatário.
  • Nos casos em que a estrutura da mensagem inclui matrizes ou structs recursivas, é necessário definir seus respectivos tipos, garantindo que sejam declaradas na ordem correta.
messageType = {
           FirstStruct: [
               { name: "owner", type: "address" },
               { name: "customer", type: "Customer[]" },
               { name: "seller", type: "Seller" },
               { name: "price", type: "uint256" },
           ],
           Customer: [
               { name: "address", type: "address" },
               { name: "discount", type: "uint256" },
           ],
           Seller: [
               { name: "address", type: "address" },
               { name: "price", type: "uint256" },
           ],
       };
Enter fullscreen mode Exit fullscreen mode
  • Lembre-se de que determinadas medidas de segurança devem ser levadas em consideração ao verificar v, r e s on-chain. Essas precauções já são tratadas se você usar a implementação ECDSA dos contratos do OpenZeppelin.

Conclusão

Está tudo pronto! Com essas informações, agora você pode assinar com confiança qualquer mensagem EIP712 e verificá-la tanto on-chain quanto off-chain. Espero que este guia, juntamente com o código de amostra fornecido, seja útil para você.

Esse artigo foi escrito por Zakrad e traduzido por Fátima Lima. O original pode ser lido aqui.

Top comments (0)