Pular para o conteúdo principal

ERC-20 com Trilhos de Segurança

erc-20
Iniciante
Ori Pomerantz
15 de agosto de 2022
9 minutos de leitura

Introdução

Uma das grandes vantagens do Ethereum é que não existe uma autoridade central que possa modificar ou desfazer suas transações. Um dos maiores problemas do Ethereum é que não há autoridade central com o poder de desfazer erros de usuário ou transações ilícitas. Neste artigo, você aprenderá sobre alguns dos erros comuns que os usuários cometem com 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 usemos o contrato de token ERC-20 da OpenZeppelin (opens in a new tab), este artigo não o explica em grandes detalhes. Você pode encontrar esta informação aqui.

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

  1. Abra o Remix IDE (opens in a new tab).
  2. Clique no ícone de clonagem do GitHub (clone github icon).
  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 a funcionalidade de trilhos de segurança, precisamos de um contrato ERC-20. Neste artigo, usaremos o Assistente de Contratos da OpenZeppelin (opens in a new tab). Abra-o em outro navegador e siga estas instruções:

  1. Selecione ERC20.

  2. Insira estas configurações:

    ParâmetroValor
    NomeSafetyRailsToken
    SímboloSAFE
    Pré-cunhagem1000
    RecursosNenhum
    Controle de acessoOwnable
    Capacidade de atualizaçãoNenhum
  3. Role para cima e clique em Abrir no Remix (para o Remix) ou Download para usar um ambiente diferente. Vou presumir que você está usando o Remix. Se estiver usando algo diferente, faça as alterações apropriadas.

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

  5. Compile, implante e interaja com o contrato para ver que ele funciona como um contrato ERC-20. Se precisar aprender a usar o Remix, use este tutorial (opens in a new tab).

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 pretendiam fazer, existem dois tipos de erro que acontecem com frequência 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 (opens in a new tab) conseguiu acumular mais de 120.000 (opens in a new tab) tokens OP em menos de dois meses. Isso representa uma quantia significativa de riqueza que, presumivelmente, as pessoas simplesmente perderam.

  2. Enviar os tokens para um endereço vazio, um que não corresponde a uma conta de propriedade externa ou a um contrato inteligente. Embora eu não tenha estatísticas sobre a frequência com que isso acontece, um incidente poderia ter custado 20.000.000 de tokens (opens in a new tab).

Impedindo transferências

O contrato ERC-20 da OpenZeppelin inclui um hook, _beforeTokenTransfer (opens in a new tab), que é chamado antes da transferência de um token. Por padrão, este hook não faz nada, mas podemos vincular nossa própria funcionalidade a ele, como verificações que revertem a transação se houver um problema.

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

1 function _beforeTokenTransfer(address from, address to, uint256 amount)
2 internal virtual
3 override(ERC20)
4 {
5 super._beforeTokenTransfer(from, to, amount);
6 }

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

1 internal virtual

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

1 override(ERC20)

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

1 super._beforeTokenTransfer(from, to, amount);

Esta linha chama a função _beforeTokenTransfer do contrato ou contratos dos quais herdamos que a possuem. Neste caso, é apenas o ERC20, o Ownable não tem este hook. Mesmo que atualmente o ERC20._beforeTokenTransfer não faça nada, nós o chamamos caso uma funcionalidade seja adicionada no futuro (e então decidirmos reimplantar 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 ser vazio, tem que ser:
    • Uma conta de propriedade externa (EOA). Não podemos verificar diretamente se um endereço é uma EOA, mas podemos verificar o saldo em ETH de um endereço. As EOAs quase sempre têm um saldo, mesmo que não sejam mais usadas — é difícil limpá-las até o último wei.
    • Um contrato inteligente. Testar se um endereço é um contrato inteligente é um pouco mais difícil. Existe um opcode que verifica o tamanho do código externo, chamado EXTCODESIZE (opens in a new tab), mas ele não está disponível diretamente no Solidity. Temos que usar o Yul (opens in a new tab), que é o assembly da EVM, para isso. Existem outros valores que poderíamos usar do Solidity (<address>.code e <address>.codehash (opens in a new tab)), mas eles custam mais.

Vamos analisar o novo código linha por linha:

1 require(to != address(this), "Não é possível enviar tokens para o endereço do contrato");

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

1 bool isToContract;
2 assembly {
3 isToContract := gt(extcodesize(to), 0)
4 }

É assim que verificamos se um endereço é um contrato. Não podemos receber saídas diretamente do Yul, então, em vez disso, definimos uma variável para manter o resultado (isToContract neste caso). A maneira como o Yul funciona é que cada opcode é considerado uma função. Então, primeiro chamamos o EXTCODESIZE (opens in a new tab) para obter o tamanho do contrato e, em seguida, usamos o GT (opens in a new tab) para verificar se não é zero (estamos lidando com inteiros sem sinal, então, claro, ele não pode ser negativo). Em seguida, escrevemos o resultado em isToContract.

1 require(to.balance != 0 || isToContract, "Não é possível enviar tokens para um endereço vazio");

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

Acesso administrativo

Às vezes, é útil ter um administrador que possa desfazer erros. Para reduzir o potencial de abuso, este administrador pode ser uma multisig (opens in a new tab) para que várias pessoas tenham que concordar com uma ação. Neste artigo, teremos dois recursos administrativos:

  1. Congelar e descongelar contas. Isso pode ser útil, por exemplo, quando uma conta pode ser comprometida.

  2. Limpeza de ativos.

    Às vezes, fraudadores enviam tokens fraudulentos para o contrato do token real para ganhar legitimidade. Por exemplo, veja aqui (opens in a new tab). O contrato ERC-20 legítimo é 0x4200....0042 (opens in a new tab). O golpe que finge ser ele é 0x234....bbe (opens in a new tab).

    Também é possível que as pessoas enviem tokens ERC-20 legítimos para nosso contrato por engano, que é outra razão para querer ter uma maneira de retirá-los.

O OpenZeppelin fornece dois mecanismos para habilitar o acesso administrativo:

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

Congelando e descongelando contratos

Congelar e descongelar contratos requer várias mudanças:

  • Um mapeamento (opens in a new tab) de endereços para booleanos (opens in a new tab) para rastrear 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.

    1 mapping(address => bool) public frozenAccounts;
  • Eventos (opens in a new tab) para informar a qualquer pessoa interessada quando uma conta é congelada ou descongelada. Tecnicamente falando, eventos não são necessários para essas ações, mas ajuda o código off-chain a ser capaz de escutar esses eventos e saber o que está acontecendo. É considerado uma boa prática que um contrato inteligente os emita quando algo que possa ser relevante para outra pessoa acontecer.

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

    1 // Quando as contas são congeladas ou descongeladas
    2 event AccountFrozen(address indexed _addr);
    3 event AccountThawed(address indexed _addr);
  • Funções para congelar e descongelar contas. Essas duas funções são quase idênticas, então analisaremos apenas a função de congelamento.

    1 function freezeAccount(address addr)
    2 public
    3 onlyOwner

    Funções marcadas como public (opens in a new tab) podem ser chamadas de outros contratos inteligentes ou diretamente por uma transação.

    1 {
    2 require(!frozenAccounts[addr], "Conta já congelada");
    3 frozenAccounts[addr] = true;
    4 emit AccountFrozen(addr);
    5 } // freezeAccount

    Se a conta já estiver congelada, a transação será revertida. Caso contrário, congele-a e emita um evento com emit.

  • Altere o _beforeTokenTransfer para impedir que o dinheiro seja movido de uma conta congelada. Observe que o dinheiro ainda pode ser transferido para a conta congelada.

    1 require(!frozenAccounts[from], "A conta está congelada");

Limpeza de ativos

Para liberar os tokens ERC-20 mantidos por este contrato, precisamos chamar uma função no contrato do token ao qual eles pertencem, seja transfer (opens in a new tab) ou approve (opens in a new tab). Não faz sentido desperdiçar gás neste caso com aprovações; é melhor transferir diretamente.

1 function cleanupERC20(
2 address erc20,
3 address dest
4 )
5 public
6 onlyOwner
7 {
8 IERC20 token = IERC20(erc20);

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

1 uint balance = token.balanceOf(address(this));
2 token.transfer(dest, balance);
3 }

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

Conclusão

Esta não é uma solução perfeita — não há solução perfeita para o problema do "usuário cometeu um erro". No entanto, o uso desses tipos 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.

Veja aqui mais do meu trabalho (opens in a new tab).

Última atualização da página: 4 de setembro de 2025

Este tutorial foi útil?