Resposta curta. O Sign-In with Ethereum (EIP-4361, Final) é uma mensagem em texto simples com campos fixos, assinada com personal_sign (EIP-191). As carteiras exibem a mensagem e a recusam se o domain dela não corresponder à página que faz o pedido. O servidor deve fazer o resto: emitir um nonce aleatório e aceitá-lo uma única vez, conferir domain, URI, Chain ID, Version e os campos de data e hora com os próprios valores e, só então, verificar a assinatura. Use ecrecover apenas para contas comuns; carteiras de contrato inteligente exigem o isValidSignature da ERC-1271 na cadeia da mensagem, e carteiras que ainda não foram implantadas assinam com o wrapper da ERC-6492. Vincule a sessão ao endereço, e não a um nome ENS ou a um saldo.
A mensagem
Uma mensagem SIWE, criada com o viem para este artigo:
app.example wants you to sign in with your Ethereum account:
0x701111bB8ee63D09066471B2b26E8a6eD017B521
Sign in to app.example.
URI: https://app.example/login
Version: 1
Chain ID: 31337
Nonce: 7d19b07372ad88a030c953465f770a48
Issued At: 2026-10-20T13:10:41.396Z
Expiration Time: 2026-10-20T13:20:41.396Z
O formato é fixado por uma gramática ABNF na EIP, e cada linha tem uma função (§ Message Fields):
| Campo | Obrigatório | Para que serve |
|---|---|---|
scheme (antes do domínio, opcional) | não | Esquema da origem; HTTPS quando ausente |
domain | sim | A origem que solicita o login; as carteiras a comparam com a página |
address | sim | A conta que assina, com checksum EIP-55 |
statement | não | Uma linha legível por humanos, sem quebra de linha no meio |
URI | sim | O recurso ao qual o login se destina |
Version | sim | Deve ser 1 |
Chain ID | sim | A cadeia à qual a sessão fica vinculada e na qual as contas de contrato devem ser resolvidas |
Nonce | sim | Aleatório, com pelo menos 8 caracteres alfanuméricos, contra repetição |
Issued At | sim | Data e hora de criação, em RFC 3339 |
Expiration Time, Not Before | não | Janela de validade da mensagem assinada |
Request ID, Resources | não | Identificadores da aplicação e URIs às quais o usuário concede acesso |
A carteira assina a mensagem com personal_sign, ou seja, a versão 0x45 da EIP-191: o keccak256 de "\x19Ethereum Signed Message:\n", do comprimento em bytes escrito em decimal e da mensagem. Como a mensagem é texto, o usuário consegue ler o que assina, ao contrário dos hashes de 32 bytes discutidos em EIP-712: o que você realmente assina.
Quem verifica o quê
A EIP-4361 divide o trabalho. As carteiras devem analisar a mensagem segundo a ABNF, comparar scheme e domain com a origem real da requisição e rejeitar qualquer divergência, além de exibir domain, address, statement e resources antes de assinar. Essa é a proteção contra phishing: uma página em app-example.login não consegue obter, de uma carteira de navegador que segue a especificação, uma assinatura sobre uma mensagem que diga app.example.
A parte confiável (relying party, RP) deve verificar a mensagem “quanto à conformidade com a ABNF”, compará-la com os “valores esperados após a análise” e verificar a assinatura, com “o contrato que realiza a verificação … resolvido a partir do ID da cadeia especificado” no caso de contas de contrato. As sessões “DEVEM ser vinculadas ao endereço”. Nada disso é automático, e uma página de phishing ainda pode coletar uma assinatura sobre uma mensagem com o domínio da própria página; por isso o servidor também precisa verificar o domínio.
Um verificador no servidor
O verificador abaixo faz essas verificações antes de tocar na assinatura e usa o verifySiweMessage do viem para a parte criptográfica. Duas decisões são deliberadas: o nonce é consumido na primeira tentativa, com ou sem sucesso, e a cadeia é aquela à qual o cliente do servidor está conectado, de modo que uma mensagem para outra cadeia nem chega à verificação da assinatura.
import { randomBytes } from "node:crypto";
import { parseSiweMessage } from "viem/siwe";
/**
* Server side of Sign-In with Ethereum (EIP-4361). `client` is a viem public client connected
* to the chain the sessions are bound to: EIP-4361 requires contract accounts to be resolved
* on the message's Chain ID.
*/
export function createSiweVerifier({ client, domain, origin, maxAgeSeconds = 300 }) {
const pending = new Map(); // nonce -> expiry (ms); one entry per sign-in attempt
return {
/** Step 1: a fresh nonce for the page that is about to ask the wallet to sign. */
issueNonce() {
const nonce = randomBytes(16).toString("hex"); // 128 bits from the OS CSPRNG, 32 alphanumeric characters
pending.set(nonce, Date.now() + maxAgeSeconds * 1000);
return nonce;
},
/** Step 2: check the signed message, then the signature; returns the session subject. */
async verify(message, signature) {
const fields = parseSiweMessage(message);
const expiry = pending.get(fields.nonce);
pending.delete(fields.nonce); // single use, whatever the outcome
if (expiry === undefined) throw new Error("unknown or already used nonce");
if (Date.now() > expiry) throw new Error("nonce expired");
if (fields.domain !== domain) throw new Error(`message is for ${fields.domain}`);
if (!fields.uri || new URL(fields.uri).origin !== origin) throw new Error(`URI ${fields.uri} is not on ${origin}`);
if (fields.version !== "1") throw new Error(`unsupported version ${fields.version}`);
if (fields.chainId !== client.chain.id) throw new Error(`message is for chain ${fields.chainId}`);
if (!fields.issuedAt || Date.now() - fields.issuedAt.getTime() > maxAgeSeconds * 1000) {
throw new Error("issued too long ago");
}
// verifySiweMessage checks domain and nonce only when they are passed, plus expiration and
// not-before, then the signature: ecrecover for plain accounts, ERC-1271 for deployed
// contract accounts, ERC-6492 for contract accounts that are not deployed yet.
const valid = await client.verifySiweMessage({ message, signature, domain, nonce: fields.nonce });
if (!valid) throw new Error("signature does not verify");
return { address: fields.address, chainId: fields.chainId }; // bind the session to the address
},
};
}
Repare no que o viem verifica e no que não verifica. Na versão 2.57.7, o verifySiweMessage compara domain, nonce e scheme apenas quando você os passa, confere Expiration Time e Not Before com o horário atual e então verifica a assinatura. Ele não olha Chain ID, URI, Version nem a idade de Issued At. Chamado só com message e signature, ele aceita uma mensagem assinada para qualquer domínio e com qualquer nonce, o que este teste curto confirma, junto com uma segunda armadilha, os finais de linha:
// Two ways a SIWE check goes wrong without any attacker skill: calling the library without the
// expected values, and changing the line endings of the message. EOA only; keys generated here.
import { createPublicClient, http } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { foundry } from "viem/chains";
import { createSiweMessage } from "viem/siwe";
const client = createPublicClient({ chain: foundry, transport: http("http://127.0.0.1:8545") });
const alice = privateKeyToAccount(generatePrivateKey());
// A message alice signed on some other site, with that site's nonce.
const elsewhere = createSiweMessage({
domain: "other.example", address: alice.address, uri: "https://other.example/", version: "1",
chainId: foundry.id, nonce: "a1b2c3d4", issuedAt: new Date(),
});
const signature = await alice.signMessage({ message: elsewhere });
console.log(`verifySiweMessage({ message, signature }) -> ${await client.verifySiweMessage({ message: elsewhere, signature })}`);
console.log(`verifySiweMessage({ message, signature, domain, nonce }) -> ${await client.verifySiweMessage({ message: elsewhere, signature, domain: "app.example", nonce: "f00dfeed" })}`);
// The same message after a transport or template turned LF into CRLF.
const crlf = elsewhere.replaceAll("\n", "\r\n");
console.log(`same message with CRLF line endings -> ${await client.verifySiweMessage({ message: crlf, signature })}`);
// What personal_sign receives over EIP-1193: the hex-encoded UTF-8 message first, then the address.
const params = [`0x${Buffer.from(elsewhere, "utf8").toString("hex")}`, alice.address];
console.log(`personal_sign params: ["${params[0].slice(0, 26)}...", "${params[1]}"]`);
verifySiweMessage({ message, signature }) -> true
verifySiweMessage({ message, signature, domain, nonce }) -> false
same message with CRLF line endings -> false
personal_sign params: ["0x6f746865722e6578616d706c...", "0xC527ED169430Ca38aADCEb03cd04f6F390b4840F"]
A primeira linha é uma assinatura válida sobre uma mensagem para other.example, aceita por uma chamada que não informa nenhum domínio. A terceira mostra por que uma mensagem SIWE precisa chegar intacta, byte a byte: a ABNF usa LF entre as linhas, e um template, um campo de formulário ou uma camada HTTP que o troque por CRLF produz outro hash EIP-191 e uma verificação com falha que parece uma assinatura inválida.
// How related are two consecutive nonces from viem's generateSiweNonce()?
import { generateSiweNonce } from "viem/siwe";
const a = generateSiweNonce();
const b = generateSiweNonce();
console.log(`first ${a}`);
console.log(`second ${b}`);
console.log(`second equals first shifted by one character: ${b.slice(0, 95) === a.slice(1)}`);
console.log(`alphabet: ${[...new Set(a + b)].sort().join("")}`);
first ef56353692864c3d4da72cbdea645d3aac4543c9527e869572a71cbd5bf1d340b2488493e175223f8453b8e6e4afae8b
second f56353692864c3d4da72cbdea645d3aac4543c9527e869572a71cbd5bf1d340b2488493e175223f8453b8e6e4afae8b7
second equals first shifted by one character: true
alphabet: 0123456789abcdef
O lado do cliente
A página que inicia o login faz três coisas: busca um nonce no servidor, monta a mensagem com o domínio e a URI da própria página e pede à carteira que a assine. Pela interface de provedor do navegador (EIP-1193), esse último passo é uma requisição personal_sign cujos parâmetros, segundo a especificação da API do MetaMask, são primeiro a mensagem em UTF-8 codificada em hex e depois o endereço, como mostra a última linha da saída de pitfalls.mjs. Funções de biblioteca como o signMessage do viem fazem essa codificação; código escrito à mão que passa o endereço primeiro, ou a string crua em vez de hex, é uma fonte comum de assinaturas sobre os bytes errados.
Depois, envie ao servidor a string exata da mensagem que foi assinada, e não os campos a partir dos quais ela foi montada. O servidor analisa e verifica essa string; reconstruí-la no servidor a partir de campos separados faz com que cada diferença de espaços em branco ou de ordem vire uma falha de verificação ou, pior, uma divergência entre o que foi verificado e o que foi assinado.
A lista Resources merece um comentário. A ERC-5573 (“ReCaps”, ainda um rascunho) usa a última entrada dela, uma URI urn:recap:, para pedir capacidades com escopo definido e acrescenta uma frase gerada à declaração (“I further authorize the stated URI to perform the following actions on my behalf…”). Se você usar esse mecanismo, o servidor precisa analisar e aplicar essas capacidades; um verificador SIWE simples as ignora.
Carteiras de contrato inteligente: ERC-1271 e ERC-6492
Uma carteira de contrato inteligente, como uma Safe ou uma conta ERC-4337, não tem chave privada própria. O ecrecover aplicado à assinatura dela retorna o endereço da chave que assinou, seja ela qual for (a chave de um proprietário, uma chave de sessão), ou nada, mas nunca o endereço da carteira. A ERC-1271 (Final) deixa a decisão para a carteira: o verificador chama isValidSignature(bytes32 hash, bytes signature) na carteira, que retorna 0x1626ba7e se a assinatura for válida pelas regras dela.
Muitas carteiras inteligentes só são implantadas quando necessário, na primeira transação, mas os usuários querem fazer login antes disso. A ERC-6492 (Final) encapsula a assinatura com o endereço do contrato de fábrica e o calldata que implantaria a carteira, seguidos do sufixo de 32 bytes 0x6492…6492. Um verificador que vê o sufixo simula a implantação em um eth_call e depois consulta a futura carteira; a ERC exige procurar o sufixo antes da ERC-1271 e antes do ecrecover. O verifySiweMessage do viem cobre os três casos por meio de verifyHash.
Para testar os três caminhos, o laboratório rodou uma cadeia local com o anvil do Foundry 1.8.5 (ghcr.io/foundry-rs/foundry:v1.8.5) e uma carteira mínima compilada com o solc 0.8.30: uma chave de proprietário, isValidSignature com ecrecover e uma fábrica CREATE2, para que o endereço seja conhecido antes da implantação.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
/// A minimal smart-contract account for the article's lab: one owner key, ERC-1271 signature checks.
contract OwnerWallet {
address public immutable owner;
constructor(address owner_) {
require(owner_ != address(0), "owner required");
owner = owner_;
}
/// ERC-1271: returns 0x1626ba7e when `signature` is the owner's signature over `hash`.
function isValidSignature(bytes32 hash, bytes calldata signature) external view returns (bytes4) {
if (signature.length != 65) return 0xffffffff;
bytes32 r = bytes32(signature[0:32]);
bytes32 s = bytes32(signature[32:64]);
uint8 v = uint8(signature[64]);
// Reject high-s values (EIP-2), so one owner signature has exactly one valid encoding.
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) return 0xffffffff;
return ecrecover(hash, v, r, s) == owner ? bytes4(0x1626ba7e) : bytes4(0xffffffff);
}
}
/// Deploys OwnerWallet with CREATE2, so a wallet's address is known before it exists (ERC-6492).
contract OwnerWalletFactory {
function deploy(address owner, bytes32 salt) external returns (address wallet) {
wallet = predict(owner, salt);
if (wallet.code.length == 0) new OwnerWallet{salt: salt}(owner);
}
function predict(address owner, bytes32 salt) public view returns (address) {
bytes32 initCodeHash = keccak256(abi.encodePacked(type(OwnerWallet).creationCode, abi.encode(owner)));
return address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xff), address(this), salt, initCodeHash)))));
}
}
O script principal financia uma chave de implantação recém-gerada, implanta o contrato de fábrica, implanta a carteira de bob e apenas prevê o endereço da de carol; depois faz login como alice (uma conta comum), como a carteira de bob e como a carteira ainda não implantada de carol e, por fim, tenta repetições e falsificações:
// Lab driver: an EOA, a deployed ERC-1271 wallet and an undeployed (ERC-6492) wallet sign in to the
// same server on a local anvil chain; then the attacks the server has to refuse. All keys are generated here.
import { readFileSync } from "node:fs";
import { createPublicClient, createTestClient, createWalletClient, encodeFunctionData, hashMessage, http, parseEther, recoverMessageAddress, serializeErc6492Signature } from "viem";
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { foundry } from "viem/chains";
import { createSiweMessage } from "viem/siwe";
import { createSiweVerifier } from "./siwe-server.mjs";
const transport = http("http://127.0.0.1:8545");
const client = createPublicClient({ chain: foundry, transport });
const deployer = privateKeyToAccount(generatePrivateKey());
await createTestClient({ chain: foundry, mode: "anvil", transport }).setBalance({ address: deployer.address, value: parseEther("10") });
const wallet = createWalletClient({ account: deployer, chain: foundry, transport });
const artifact = (name) => JSON.parse(readFileSync(`/chain/out/OwnerWallet.sol/${name}.json`, "utf8"));
const { abi: factoryAbi, bytecode } = artifact("OwnerWalletFactory");
const deployTx = await wallet.deployContract({ abi: factoryAbi, bytecode: bytecode.object });
const factory = (await client.waitForTransactionReceipt({ hash: deployTx })).contractAddress;
const predict = (owner, salt) => client.readContract({ address: factory, abi: factoryAbi, functionName: "predict", args: [owner, salt] });
const deployWallet = async (owner, salt) => {
const hash = await wallet.writeContract({ address: factory, abi: factoryAbi, functionName: "deploy", args: [owner, salt] });
await client.waitForTransactionReceipt({ hash });
};
const server = createSiweVerifier({ client, domain: "app.example", origin: "https://app.example" });
const messageFor = (address, overrides = {}) =>
createSiweMessage({
domain: "app.example",
address,
statement: "Sign in to app.example.",
uri: "https://app.example/login",
version: "1",
chainId: foundry.id,
nonce: server.issueNonce(),
issuedAt: new Date(),
expirationTime: new Date(Date.now() + 10 * 60_000),
...overrides,
});
// Three users. alice: a plain account. bob: a deployed smart wallet. carol: a smart wallet not deployed yet.
const alice = privateKeyToAccount(generatePrivateKey());
const bob = privateKeyToAccount(generatePrivateKey());
const carol = privateKeyToAccount(generatePrivateKey());
const salt = `0x${"00".repeat(31)}01`;
await deployWallet(bob.address, salt);
const bobWallet = await predict(bob.address, salt);
const carolWallet = await predict(carol.address, salt);
const aliceMessage = messageFor(alice.address);
console.log(`${aliceMessage}\n`);
const attempts = [];
attempts.push(["EOA, personal_sign", aliceMessage, await alice.signMessage({ message: aliceMessage })]);
const bobMessage = messageFor(bobWallet);
attempts.push(["deployed wallet, ERC-1271", bobMessage, await bob.sign({ hash: hashMessage(bobMessage) })]);
const carolMessage = messageFor(carolWallet);
attempts.push([
"undeployed wallet, ERC-6492",
carolMessage,
serializeErc6492Signature({
address: factory,
data: encodeFunctionData({ abi: factoryAbi, functionName: "deploy", args: [carol.address, salt] }),
signature: await carol.sign({ hash: hashMessage(carolMessage) }),
}),
]);
console.log(`carol's wallet has code before sign-in: ${Boolean(await client.getCode({ address: carolWallet }))}`);
for (const [label, message, signature] of attempts) {
let recovered;
try {
recovered = await recoverMessageAddress({ message, signature });
} catch (e) {
recovered = e.shortMessage ?? e.message;
}
const claimed = message.split("\n")[1];
const result = await server.verify(message, signature).then((s) => `session for ${s.address.slice(0, 10)}...`, (e) => `rejected: ${e.message}`);
console.log(`${label.padEnd(28)} ${result}; ecrecover alone gives ${recovered === claimed ? "the same address" : String(recovered).slice(0, 42)}`);
}
console.log(`carol's wallet has code after sign-in: ${Boolean(await client.getCode({ address: carolWallet }))}`);
// What the server must refuse.
const check = async (label, message, signature) => {
const result = await server.verify(message, signature).then(() => "ACCEPTED", (e) => `rejected: ${e.message}`);
console.log(`${label.padEnd(36)} ${result}`);
};
const replayed = messageFor(alice.address);
const replayedSig = await alice.signMessage({ message: replayed });
await server.verify(replayed, replayedSig);
await check("same message and signature again", replayed, replayedSig);
const phish = messageFor(alice.address, { domain: "app-example.login", uri: "https://app-example.login/" });
await check("signed on a look-alike site", phish, await alice.signMessage({ message: phish }));
const otherChain = messageFor(alice.address, { chainId: 1 });
await check("message for chain 1", otherChain, await alice.signMessage({ message: otherChain }));
const stale = messageFor(alice.address, { issuedAt: new Date(Date.now() - 3600_000), expirationTime: new Date(Date.now() + 3600_000) });
await check("issued an hour ago", stale, await alice.signMessage({ message: stale }));
const original = messageFor(alice.address);
const originalSig = await alice.signMessage({ message: original });
await check("statement edited after signing", original.replace("Sign in to app.example.", "Sign in to app.example as admin."), originalSig);
const mallory = privateKeyToAccount(generatePrivateKey());
const claimsBob = messageFor(bobWallet);
await check("bob's wallet, signed by someone else", claimsBob, await mallory.sign({ hash: hashMessage(claimsBob) }));
carol's wallet has code before sign-in: false
EOA, personal_sign session for 0x701111bB...; ecrecover alone gives the same address
deployed wallet, ERC-1271 session for 0x5d3a68A0...; ecrecover alone gives 0xE4e0d879c3a051245cc489dE8Ae281c019691864
undeployed wallet, ERC-6492 session for 0x6F47E5F9...; ecrecover alone gives invalid signature length
carol's wallet has code after sign-in: false
same message and signature again rejected: unknown or already used nonce
signed on a look-alike site rejected: message is for app-example.login
message for chain 1 rejected: message is for chain 1
issued an hour ago rejected: issued too long ago
statement edited after signing rejected: signature does not verify
bob's wallet, signed by someone else rejected: signature does not verify
O que a execução mostra:
- Os três tipos de conta fizeram login pelo mesmo caminho de código. Para a carteira de bob, o
ecrecoverretorna a chave do proprietário, um endereço diferente do que está na mensagem; um servidor que compara esse resultado comaddressrejeita todo usuário de carteira inteligente, e um que, em vez disso, aceita o proprietário abre a sessão do usuário na conta errada. Para a carteira não implantada de carol, oecrecovernem consegue interpretar a assinatura encapsulada. - A verificação ERC-6492 não implanta nada. A carteira de carol não tinha código nem antes nem depois do login; a implantação aconteceu apenas dentro da chamada simulada.
- Cada falsificação falhou na verificação certa: a repetição no nonce, o site imitador no domínio, a outra cadeia no ID da cadeia, a mensagem antiga em
Issued At, a declaração editada e o signatário errado na assinatura.
Depois da assinatura: sessões
- Vincule a sessão ao endereço e à cadeia. A EIP-4361 diz que as sessões “DEVEM ser vinculadas ao
address, e não a outros recursos resolvidos a partir dele que podem mudar”, como um nome ENS ou um saldo de tokens. Verifique esses dados de novo quando for usá-los. - Carteiras de contrato podem mudar de ideia. Os resultados da ERC-1271 dependem do estado da cadeia: proprietários podem ser trocados, e chaves de sessão, revogadas. A EIP sugere invalidar as sessões quando o estado por trás de uma assinatura muda; no mínimo, mantenha curtas as sessões de contas de contrato.
- Emita sua própria credencial de sessão (um cookie ou token) após a verificação e nunca aceite a própria assinatura SIWE como credencial do tipo bearer em requisições posteriores; por concepção, dentro da janela de validade, nada além do nonce que você já consumiu impede que ela seja repetida.
- O campo Expiration Time (data de expiração) limita a mensagem, não a sessão. Ele delimita o período em que a assinatura pode ser apresentada; a duração da sessão é uma decisão separada.
Quando o SIWE é a ferramenta certa
O SIWE autentica uma conta, não uma pessoa. Ele é adequado quando a conta Ethereum é a identidade que importa: uma aplicação que age sobre os ativos on-chain do usuário, verifica uma associação condicionada à posse de tokens (token-gated) ou quer a mesma identidade em vários serviços sem endereço de e-mail. Como único login de um serviço web comum, é uma escolha ruim: uma chave perdida não tem “esqueci minha senha”, não há garantia de uma conta por usuário, e as próprias considerações de segurança da EIP-4361 apontam o custo de privacidade de reutilizar o mesmo endereço em todo lugar. Para contas de usuário tradicionais, as chaves de acesso (passkeys) oferecem login resistente a phishing sem carteira; para identidade federada, o OpenID Connect fornece um emissor e um titular verificados. Muitas aplicações combinam as duas coisas: uma sessão com chave de acesso ou OIDC para a pessoa e uma assinatura SIWE para vincular um endereço a ela.
Erros comuns
- Montar a mensagem no servidor e confiar que o cliente vai assiná-la sem alterações. Verifique o que foi assinado, campo por campo; é o cliente que escolhe o que envia de volta.
- Comparar
domaincom o cabeçalhoHost. Atrás de um proxy, use o domínio público que você configurou. - Não armazenar os nonces. Um nonce que nunca é conferido com o conjunto dos nonces emitidos não impede nada.
- Verificar carteiras inteligentes na cadeia errada. O mesmo endereço pode ser outro contrato, ou nenhum contrato, em outra cadeia; resolva no
Chain IDda mensagem e recuse as cadeias que você não atende. - Usar
ecrecoverem todos os casos. Ele rejeita usuários de carteiras inteligentes ou, pior, atribui o login deles à chave do proprietário. - Comparar endereços diferenciando maiúsculas de minúsculas. O checksum EIP-55 mistura maiúsculas e minúsculas; compare os endereços sem diferenciar maiúsculas de minúsculas ou após a normalização.
Fontes
- EIP-4361: Sign-In with Ethereum: campos da mensagem, ABNF, etapas da parte confiável e da carteira, considerações de segurança
- EIP-191: padrão de dados assinados
- ERC-1271: método padrão de validação de assinaturas para contratos
- ERC-6492: validação de assinaturas de contratos antes da implantação
- ERC-5573: capacidades do Sign-In with Ethereum, ReCaps (rascunho)
- Especificação da API do MetaMask: parâmetros de
personal_sign - SIWE no viem:
createSiweMessage,parseSiweMessage,verifySiweMessage - Foundry: anvil e forge