Sumário
..... . Desbloqueio da Ethereum para as massas
Quatro fundamentos de um site Web 1.0
1 . Contexto
2 . Os padrões
5 . Criando a assinatura off-chain
6 . Conclusão
Desbloqueio da Ethereum para as massas
Todos falam em transações Ethereum "sem gás", porque ninguém gosta de pagar por gás. Mas a rede Ethereum funciona precisamente porque as transações são pagas. Então, como você pode ter algo "sem gás"? O que é esta feitiçaria?
Neste artigo vou mostrar como usar os padrões por trás das transações "sem gás". Você descobrirá que, embora não exista almoço grátis na Ethereum, você pode mudar os custos do gás de maneiras interessantes.
Ao aplicar o conhecimento deste artigo, seus usuários economizarão gás, desfrutarão de um UX melhor e até mesmo construirão novos padrões de delegação em seus contratos inteligentes.
Mas espere! Há mais! Para sua conveniência, eu coloquei todas as ferramentas necessárias neste repositório. Assim, agora a barreira para você implementar tokens "sem gás" é de repente muito menor.
Vamos ser nerds.
Contexto
Devo confessar que mesmo que eu saiba como implementar transações "sem gás" em contratos inteligentes, sei muito pouco sobre a criptografia que as torna possíveis. Isso não foi um grande obstáculo para mim, portanto, também não deveria ser para você.
Tanto quanto sei, minha chave privada é usada para assinar as transações que envio a Ethereum, e alguma mágica criptográfica é usada para me identificar como remetente da mensagem. Isso está subjacente a todo o controle de acesso na Ethereum.
A magia por trás das transações "sem gás" é que eu posso produzir uma assinatura com minha chave privada e a transação de contrato inteligente que eu quero que seja executada.
A assinatura seria produzida off-chain, sem gastar nada com gás. Então eu poderia dar esta assinatura a outra pessoa para executar a transação em meu nome, com seu gás.
A função para a qual a assinatura é destinada será normalmente uma função regular, mas ampliada com parâmetros adicionais de assinatura. Por exemplo, no dai.sol, temos a função de aprovação:
function approve(address usr, uint wad) external returns (bool)
Temos também a função permit
, que faz o mesmo que approve
, mas toma uma assinatura como parâmetro.
function permit(address holder, address spender, uint256 nonce, uint256 expiry, bool allowed, uint8 v, bytes32 r, bytes32 s) external
Não se preocupe com todos esses parâmetros extras, nós vamos chegar até eles. O que você precisa prestar atenção é ao que ambas as funções fazem com o mapeamento allowance
:
function approve(address usr, uint wad) external returns (bool)
{
allowance[msg.sender][usr] = wad;
...
}
function permit(
address holder, address spender,
uint256 nonce, uint256 expiry, bool allowed,
uint8 v, bytes32 r, bytes32 s
) external {
...
allowance[holder][spender] = wad;
...
}
Se você usar approve
, você permite que spender
use até wad
de seus tokens.
Se você der uma assinatura válida a alguém, essa pessoa pode chamar permit
para autorizar que spender
utilize seus tokens.
Portanto, basicamente, o padrão por trás das transações "sem gás" é criar uma assinatura que você pode dar a alguém, para que ela possa executar com segurança uma transação especial. É como dar permissão a alguém para executar uma função.
É um padrão de delegação.
Os padrões
Se você é como eu, a primeira coisa que você fará é mergulhar no código. Imediatamente reparei neste comentário:
// -- -- delicadezas EIP712 -- -
Com isso, desci pela toca do coelho e me perdi irremediavelmente. Agora que o entendi, posso explicar em termos simples.
EIP712 descreve como construir assinaturas para funções, de uma maneira genérica. Outras EIPs descrevem como aplicar a EIP712 a casos específicos de uso. Por exemplo, a EIP2612 descreve como utilizar as assinaturas EIP712 para uma função chamada permit
que deve ter a mesma funcionalidade que approve
em um token ERC20.
Se você quiser apenas implementar uma função de assinatura que já foi feita antes, como adicionar assinaturas aprovadas em seu próprio MetaCoin, então você pode ler a EIP2612 e estará bem encaminhado. Você pode até herdar de um contrato que o implemente e limitar o estresse em sua vida.
Neste artigo vamos investigar uma implementação de transações "sem gás" no dai.sol, o que deixará as coisas claras. A implementação do dai.sol aconteceu antes da EIP2612 e é ligeiramente diferente. Isso não será um problema.
Composição da assinatura
Uma implementação antecipada das assinaturas da EIP712 pode ser encontrada no dai.sol. Ela permite que os portadores de dai aprovem transações de transferência calculando uma assinatura off-chain e dando-a ao gastador, em vez de chamarem a aprovação por eles mesmos.
Ela inclui quatro elementos:
- Um
DOMAIN_SEPARATOR
. - Um
PERMIT_TYPEHASH
. - Uma variável
nonces
. - Uma função
permit
.
Este é o DOMAIN_SEPARATOR
, com variáveis relacionadas:
string public constant name = "Dai Stablecoin";
string public constant version = "1";
bytes32 public DOMAIN_SEPARATOR;
constructor(uint256 chainId_) public {
...
DOMAIN_SEPARATOR = keccak256(abi.encode(
keccak256(
"EIP712Domain(string name,string version," +
"uint256 chainId,address verifyingContract)"
),
keccak256(bytes(name)),
keccak256(bytes(version)),
chainId_,
address(this)
));
}
O DOMAIN_SEPARATOR
nada mais é do que um hash que identifica de forma única um contrato inteligente. Ele é construído a partir de uma string que o designa como um domínio EIP712, o nome do contrato simbólico, a versão, a chainId caso ele mude, e o endereço onde o contrato é implantado.
Todas essas informações são incorporadas na variávelDOMAIN_SEPARATOR
, que terá que ser usada pelo titular ao criar a assinatura, e terá que coincidir ao executar a licença. Isso garante que uma assinatura seja válida apenas para um contrato.
Esta é a variável PERMIT_TYPEHASH
:
O PERMIT_TYPEHASH
é o hash do nome da função (em maiúsculas) e de todos os parâmetros, incluindo tipo e nome. Seu objetivo é identificar claramente para qual função é a assinatura.
A assinatura será processada na função permissão, e se o PERMIT_TYPEHASH
utilizado não foi para esta função específica, ele será revertido. Isto garante que uma assinatura seja usada apenas para a função pretendida.
Em seguida, há o mapeamento de nonces
:
mapping (address => uint) public nonces;
Este mapeamento registra quantas assinaturas foram utilizadas para um determinado titular. Quando criar a assinatura, um valor nonces
precisa ser incluído. Ao executar permit
, o nonce incluído deve corresponder exatamente ao número de assinaturas que foram utilizadas até o momento para esse titular. Isto assegura que cada assinatura seja utilizada apenas uma vez.
Todas estas três condições juntas, o PERMIT_TYPEHASH
, o DOMAIN_SEPARATOR
e o nonce
, garantem que cada assinatura seja usada somente para o contrato pretendido, a função pretendida, e somente uma vez.
Agora vamos ver como a assinatura seria processada no contrato inteligente.
A função da licença
permit
é a função dai.sol que permite o uso de assinaturas para modificar allowance
do holder
em relação ao spender
.
// --- Aprovar por assinatura ---
function permit(
address holder, address spender,
uint256 nonce, uint256 expiry, bool allowed,
uint8 v, bytes32 r, bytes32 s
) external;
Como você pode ver, existem muitos parâmetros lá. Eles são todos os parâmetros necessários para calcular a assinatura, mais v,r
e s
que são a própria assinatura.
Parece bobo que você precise dos parâmetros que foram usados para criar a assinatura, mas você precisa. A única coisa que você pode recuperar da assinatura é o endereço que a criou, nada mais. Usaremos todos os parâmetros e o endereço recuperado para garantir que a assinatura seja válida.
Primeiro calculamos um digest
usando todos os parâmetros que precisaremos para garantir a segurança. O holder
precisará calcular exatamente o mesmo resumo off-chain, como parte da criação da assinatura:
bytes32 digest =
keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
keccak256(abi.encode(
PERMIT_TYPEHASH,
holder,
spender,
nonce,
expiry,
allowed
))
));
Usando ecrecover
e a assinatura v,r,s
podemos recuperar um endereço. Se for o endereço do holder
, sabemos que todos os parâmetros correspondem, DOMAIN_SEPARATOR
, PERMIT_TYPEHASH
, nonce
, holder
, spender
, expiry
, e allowed
. Se alguma coisa estiver errada, a assinatura é rejeitada:
require(holder == ecrecover(digest, v, r, s), "Dai/invalid-permit");
Uma palavra de atenção aqui. Há muitos parâmetros que entram em uma assinatura, alguns deles obscuros como chainId
(parte do DOMAIN_SEPARATOR
). Qualquer um deles, estando desativado, causará a rejeição da assinatura com exatamente o mesmo erro, o que garante que a depuração de assinaturas off-chain será difícil. Você foi avisado.
Agora sabemos que o holder
aprovou esta chamada de função. Em seguida, certificaremos que a assinatura não está sendo violada. Verificamos que a hora atual é antes de expiry
, o que permite que as licenças sejam mantidas apenas por um período específico.
require(expiry == 0 || now <= expiry, "Dai/permit-expired");
Também verificamos se uma assinatura com esse nonce
ainda não foi usada, de modo que cada assinatura pode ser usada apenas uma vez.
require(nonce == nonces[holder]++, "Dai/invalid-nonce");
E nós terminamos! a dai.sol maximiza a allowance
do holder
em relação ao spender
, emite um evento, e pronto.
uint wad = allowed ? uint(-1) : 0;
allowance[holder][spender] = wad;
emit Approval(holder, spender, wad);
O contrato dai.sol tem uma abordagem binária em relação à allowance
, no repositório, desde que você encontre um comportamento mais tradicional.
Criando a assinatura off-chain
Criar a assinatura não é para os fracos de coração, mas com um pouco de prática e persistência ela pode ser dominada. Vamos replicar o que o contrato inteligente faz na permit
em três etapas:
- Gerar o
DOMAIN_SEPARATOR
- Gerar o
digest
- Criar a assinatura da transação
A função a seguir criará DOMAIN_SEPARATOR
. É o mesmo código do construtor dai.sol, mas em JavaScript e usando keccak256
, defaultAbiCoder
e toUtfBytes
de ethers.js. Ele precisa do nome do token e do endereço de implantação, juntamente com chainId
. Ele assume que a versão do token é “1”.
A função a seguir criará um digest
para uma chamada específica permit
. Note que holder
, o spender
, nonce
e expiry
são passados como argumentos. Também passa um argumento approve.allowed
para a clareza, embora você possa defini-lo sempre como true
, caso contrário, a assinatura seria rejeitada e qual seria o objetivo? O PERMIT_TYPEHASH
que acabamos de copiar do dai.sol.
Uma vez que temos uma digest
, assinar é relativamente fácil. Apenas usamos o ecsign
de ethereumjs-util depois de remover o prefixo 0x do digest
. Note que precisamos da chave privada do usuário para fazer isso.
No código, chamaríamos estas funções da seguinte forma:
Observe como a chamadapermit
reutiliza todos os parâmetros que foram usados para criar o digest
, antes que ela fosse assinada. Somente nesse caso, a assinatura seria válida.
Observe também que as duas únicas transações neste trecho estão sendo chamadas pelo user2
. user1
é o holder
, e é o que criou o digest
e o assinou. Entretanto, o user1
não gastou nenhum gás fazendo isso.
user1
deu a assinatura ao user2
, que a utilizou para executar tanto permit
quanto a transferFrom
daquele user1
permitido.
Do ponto de vista do user1
, foi uma transação "sem gás". Ele não gastou um wei.
Conclusão
Este artigo mostra como usar transações "sem gás", esclarecendo que "sem gás" na verdade significa passar o custo do gás para outra pessoa. Para fazer isso, precisamos de uma função em um contrato inteligente que esteja pronto para lidar com transações pré-assinadas, e uma boa dose de manipulação de dados para tornar tudo seguro.
No entanto, há ganhos significativos com o uso deste padrão e, por essa razão, ele é amplamente utilizado. As assinaturas permitem passar o custo do gás da transação do usuário para o fornecedor do serviço, eliminando uma barreira considerável em muitos casos. Também permite a implementação de padrões de delegação mais avançados, muitas vezes com melhorias consideráveis de UX.
Um repositório foi providenciado para que você possa começar. Por favor, use-o e, por favor, continue a conversa.
Agradecimentos especiais a Georgios Konstantinopoulos, que me ensinou tudo o que eu sei sobre este padrão.
Artigo escrito por Alberto Cuesta Cañada. Sua versão original pode ser encontrada aqui. Traduzido e adaptado por Marcelo Panegali
Top comments (0)