WEB3DEV

Cover image for ERC-20 COM SALVAGUARDAS
Adriano P. Araujo
Adriano P. Araujo

Posted on

ERC-20 COM SALVAGUARDAS

INTRODUÇÃO

Uma das grandes vantagensda Ethereum é que não há autoridade central que possa modificar ou desfazer suas transações. Um dos grandes problemas da Ethereum é que não há autoridade central com o poder de desfazer erros de usuário ou transações ilícitas. Neste artigo, você aprende sobre alguns dos erros comuns os quais os usuários cometem com os tokens ERC-20, bem como a criar contratos ERC-20 que ajudam os usuários a evitar esses erros ou que dão a uma autoridade central algum poder (por exemplo, para congelar contas ).

Observe que, embora  usaremos o contrato de token OpenZeppelin ERC-20 , este artigo não explica isso detalhadamente. Você pode encontrar informações sobre aqui.

Se você quiser ver o código fonte completo:

  1. Abra o IDE Remix.

  2. Clique no ícone “clonar do github” ().

  3. Clone o repositório do github https://github.com/qbzzt/20220815-erc20-safety-rails.

  4. Abra contracts > erc20-safety-rails.sol.

CRIANDO UM CONTRATO ERC-20

Antes de podermos adicionar  funcionalidades de salvaguardas, precisamos de um contrato ERC-20. Neste artigo, usaremos o   OpenZeppelin Contracts Wizard. Abra-o em outro navegador e siga estas instruções:

  1. Selecione ERC20. 

  2. Digite estas configurações:

     

  1. Role para cima e clique em Open in Remix ( para Remix ) ou Download para usar um ambiente diferente. Vou assumir que você está usando o Remix.Se você usar outra coisa, faça as alterações apropriadas.

  2. Agora temos um contrato ERC-20 totalmente funcional. Você pode expandir o .deps > npm para ver o código importado.

  3. Compile, implante e brinque com o contrato para verificar se ele funciona como um contrato ERC-20. Se você precisar aprender a usar o Remix, use este tutorial.

ERROS COMUNS

Os erros

Às vezes, os usuários enviam tokens para o endereço errado. Embora não possamos ler suas mentes para saber o que eles queriam fazer, existem dois tipos de erros que acontecem muito e são fáceis de detectar:

  1. Enviar os tokens para o próprio endereço do contrato. Por exemplo, o token OP da Optimism conseguiu acumular mais de 120.000 Tokens OP em menos de dois meses. Isso representa uma quantidade significativa de riqueza que presumivelmente as pessoas acabaram perdendo.

  2. Enviar os tokens para um endereço vazio, que não corresponde a uma conta de propriedade externa ou a um contrato inteligente. Embora eu não tenha estatísticas sobre quantas vezes isso acontece, um incidente poderia custar 20.000.000 tokens.

Prevenção de transferências

O contrato OpenZeppelin ERC-20 inclui um _hook_, _beforeTokenTransfer, que é chamado antes que um token seja transferido. Por padrão, esse hook não faz nada, mas podemos adicionar nossas próprias funcionalidades, como verificações que revertem se houver algum problema.

Para usar o_hook_, adicione esta função após o construtor:


    function _beforeTokenTransfer(address from, address to, uint256 amount)
        internal virtual
        override(ERC20)
    {
        super._beforeTokenTransfer(from, to, amount);
    }

Enter fullscreen mode Exit fullscreen mode

Algumas partes dessa função podem ser novas se você não estiver muito familiarizado com o Solidity:

        internal virtual
Enter fullscreen mode Exit fullscreen mode

A palavra-chave virtual significa que, assim como herdamos a funcionalidade de ERC20 e substituímos essa função, outros contratos podem herdar de nós e substituir essa função.

        override(ERC20)
Enter fullscreen mode Exit fullscreen mode

Temos que especificar explicitamente que estamos substituindo a definição de token ERC20 de _beforeTokenTransfer. Em geral, as definições explícitas são muito melhores, do ponto de vista da segurança, do que as implícitas - você não pode esquecer que fez algo se estiver bem na sua frente. Essa também é a razão pela qual precisamos especificar quais superclasses  _beforeTokenTransfer nós estamos substituindo.

 super._beforeTokenTransfer(from, to, amount);

Enter fullscreen mode Exit fullscreen mode

Essa linha chama a função _beforeTokenTransfer do contrato ou dos contratosdos quais a herdamos. Nesse caso, é apenas o ERC20, o Ownablenão possui esse hook. Mesmo que atualmente o ERC20._beforeTokenTransfer não faça nada, o chamamos para o caso de a funcionalidade ser adicionada no futuro (e depois decidimos se reimplementaremos o contrato, porque os contratos não mudam após a implantação ).

Codificando os requisitos

Queremos adicionar estes requisitos à função:

  • O endereço to  não pode ser igual a address(this), o endereço do próprio contrato ERC-20.

  • O endereço to não pode estar vazio, deve ser:

  • Uma conta de propriedade externa (EOA). Não podemos verificar se um endereço é um EOA diretamente, mas podemos verificar o saldo de ETH de um endereço. Os EOAs quase sempre têm um saldo, mesmo que não sejam mais usados - é difícil limpá-los até o último wei.

  • Um contrato inteligente. Testar se um endereço é um contrato inteligente é um pouco mais difícil. Há um código de operação (opcode) que verifica o comprimento do código externo, chamado [EXTCODESIZE](https://www.evm.codes/#3b), mas não está disponível diretamente no Solidity. Temos que usar Yul, que é montagem EVM, para isso. Existem outros valores que poderíamos usar do Solidity (.code e .codehash), mas eles custam mais.

Vamos revisar o novo código linha por linha:

 require(to != address(this), "Can't send tokens to the contract address");

Enter fullscreen mode Exit fullscreen mode

Este é o primeiro requisito, verifique se to e this(address) não são a mesma coisa.

  bool isToContract;
        assembly {
           isToContract := gt(extcodesize(to), 0)
        }

Enter fullscreen mode Exit fullscreen mode

É assim que verificamos se um endereço é um contrato. Não podemos receber saída diretamente do Yul; portanto, definimos uma variável para manter o resultado (isToContract, neste caso ). A maneira como o Yul funciona é que todo código de operação é considerado uma função. Então, primeiro chamamos o  EXTCODESIZE para obter o tamanho do contrato e depois usamos o GT para verificar se não é zero (estamos lidando com números inteiros não assinados, portanto, é claro que não pode ser negativo). Em seguida, escrevemos o resultado para isToContract.

require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");

Enter fullscreen mode Exit fullscreen mode

E, finalmente, temos a verificação real de endereços vazios.

ACESSO ADMINISTRATIVO

Às vezes, é útil contar com um administrador capaz de corrigir erros. Para minimizar o potencial de abuso, esse administrador pode ser implementado como um esquema multisig , exigindo o consenso de várias pessoas para a realização de uma ação. Neste artigo, abordaremos dois recursos administrativos:

  1. Contas Congeladas e descongeladas. Isso pode ser útil, por exemplo, quando uma conta pode estar comprometida.

  2. Limpeza de ativos.

       Às vezes, as fraudes enviam tokens fraudulentos ao contrato do token real para obter legitimidade. Por exemplo, veja aqui. O contrato legítimo do ERC-20 é 0x4200 .... 0042. O golpe finge ser é 0x234 .... bbe.

      Também é possível que as pessoas enviem tokens legítimos do ERC-20 para o nosso contrato por engano, o que é outro motivo para querer ter uma maneira de tirá-los.

O OpenZeppelin fornece dois mecanismos para permitir o acesso administrativo:

  • Contratos Ownable têm um único proprietário. Funções que possuem o modificador onlyOwner só podem ser chamadas por esse proprietário. Os proprietários podem transferir a propriedade para outra pessoa ou renunciá-la completamente. Os direitos de todas as outras contas são tipicamente idênticos.

  • Contratos AccessControl têm controle de acesso baseado em ( RBAC ).

Por uma questão de simplicidade, neste artigo usamos Ownable.

Contratos Congelados e descongelados

Contratos de congelados e descongelados requerem várias alterações:

  • O mapeamento de endereços para booleanos para acompanhar quais endereços estão congelados. Todos os valores são inicialmente zero, o que para valores booleanos é interpretado como falso. É isso que queremos porque, por padrão, as contas não são congeladas.
    mapping(address => bool) public frozenAccounts;
Enter fullscreen mode Exit fullscreen mode
  • Eventos para informar qualquer pessoa interessada quando uma conta estiver congelada ou descongelada. Tecnicamente, os eventos não são necessários para essas ações, mas ajudam o código da cadeia a poder ouvir esses eventos e saber o que está acontecendo. É considerado uma boa maneira de um contrato inteligente emiti-los quando algo que deveria ser relevante para outra pessoa acontece.

Os eventos são indexados, portanto, será possível procurar todas as vezes que uma conta foi congelada ou descongelada.

  // Quando contas são congeladas ou descongeladas

  event AccountFrozen(address indexed _addr);

  event AccountThawed(address indexed _addr);

Enter fullscreen mode Exit fullscreen mode
  • Funções para contas de congeladas e descongeladas. Essas duas funções são quase idênticas, portanto, apenas repassaremos apenas a função de congelamento.
    function freezeAccount(address addr)
      public
      onlyOwner

Enter fullscreen mode Exit fullscreen mode

Funções marcadas com  public podem ser chamadas de outros contratos inteligentes ou diretamente por uma transação.


  {

      require(!frozenAccounts[addr], "Account already frozen");
      frozenAccounts[addr] = true;
      emit AccountFrozen(addr);
  }  // freezeAccount

Enter fullscreen mode Exit fullscreen mode

Se a conta já estiver congelada, volte. Caso contrário, congele-a e emita(emit) um evento.

  • Mude _beforeTokenTransfer para impedir que o dinheiro seja transferido de uma conta congelada. Observe que o dinheiro ainda pode ser transferido para a conta congelada.
     require(!frozenAccounts[from], "The account is frozen");
Enter fullscreen mode Exit fullscreen mode

Limpeza de ativos

Para liberar tokens ERC-20 mantidos por este contrato, precisamos chamar uma função no contrato de token ao qual eles pertencem, transfer ou approve. Não faz sentido desperdiçar gás neste caso em licenças, é melhor transferirmos diretamente.

    function cleanupERC20(
        address erc20,
        address dest)

        public
        onlyOwner

    {
        IERC20 token = IERC20(erc20);


Enter fullscreen mode Exit fullscreen mode

Essa é a sintaxe para criar um objeto para um contrato quando recebermos o endereço. Podemos fazer isso porque temos a definição de tokens ERC20 como parte do código fonte ( consulte a linha 4 ), e esse arquivo inclui a definição para IERC20, a interface para um contrato OpenZeppelin ERC-20.


        uint balance = token.balanceOf(address(this));

        token.transfer(dest, balance);

    }



Enter fullscreen mode Exit fullscreen mode

Essa é uma função de limpeza, portanto, presumivelmente, não queremos deixar nenhum token. Em vez de obter o saldo do usuário manualmente, podemos também automatizar o processo.

CONCLUSÃO

Esta não é uma solução perfeita - não há uma solução perfeita para o problema "usuário cometeu um erro”. No entanto, o uso desse tipo de verificação pode pelo menos evitar alguns erros. A capacidade de congelar contas, embora perigosa, pode ser usada para limitar os danos de certos hacks, negando ao hacker os fundos roubados.


Este artigo foi escrito por Pandapip1 e traduzido por Adriano P. de Araujo. O original em inglês pode ser encontrado aqui.

Latest comments (0)