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"
}
]
}
Depois de definir o TypedMessage, enviaremos os valores na mesma estrutura:
const domainData = {
name: "Polytrade",
version: "1.0",
chainId: chainId,
verifyingContract:
verifyingContractAddress
};
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"
}
]
}
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
}
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 )
signature = await signer._signTypedData(
domainData,
permitType,
permitData
);
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);
}
}
)
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)
ethSigUtil.signTypedData(privateKey, {
data: {
types: {
EIP712Domain:domainTye,
Permit: permitType
},
domain: domainData,
primaryType: 'Permit',
message: permitData
},
"V4"
});
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);
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);
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",
")"
)
);
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",
")"
)
);
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)
)
);
Para o PERMIT
, seguiremos o mesmo processo:
bytes32 PERMIT = keccak256(
abi.encode(
_PERMIT_TYPEHASH,
owner,
spender,
value,
nonce,
deadline
)
);
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}
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)
}
Aqui está a versão Solidity do código que executa as mesmas operações:
digest = keccak256(abi.encodePacked(uint16(0x1901), _DOMAIN_SEPARATOR, PERMIT));
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);
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]);
}
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çãorecoverAddress(digest, signature)
da biblioteca ethers ou a funçãorecoverTypedSignature(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" },
],
};
- Lembre-se de que determinadas medidas de segurança devem ser levadas em consideração ao verificar
v
,r
es
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)