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()
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
}))
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))
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;
}
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.
Oldest comments (0)