WEB3DEV

Cover image for Como Codificar Tokens Sem Gás na Ethereum
Panegali
Panegali

Posted on • Atualizado em

Como Codificar Tokens Sem Gás na Ethereum

Sumário

..... . Desbloqueio da Ethereum para as massas
Quatro fundamentos de um site Web 1.0

1 . Contexto

2 . Os padrões

3 . Composição da assinatura

4 . A função da licença

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  ...
}
Enter fullscreen mode Exit fullscreen mode

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 -- -
Enter fullscreen mode Exit fullscreen mode

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:

  1. Um DOMAIN_SEPARATOR.
  2. Um PERMIT_TYPEHASH.
  3. Uma variável nonces.
  4. 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)
  ));
}
Enter fullscreen mode Exit fullscreen mode

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:

3

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 sque 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
    ))
  ));
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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:

  1. Gerar o DOMAIN_SEPARATOR
  2. Gerar o digest
  3. 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”.

2

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.

ba

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:

bo

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)