ERC-20 com Trilhos de Segurança
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:
- Abra o Remix IDE (opens in a new tab).
- Clique no ícone de clonagem do GitHub (
). - Clone o repositório do GitHub
https://github.com/qbzzt/20220815-erc20-safety-rails. - 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:
-
Selecione ERC20.
-
Insira estas configurações:
Parâmetro Valor Nome SafetyRailsToken Símbolo SAFE Pré-cunhagem 1000 Recursos Nenhum Controle de acesso Ownable Capacidade de atualização Nenhum -
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.
-
Agora, temos um contrato ERC-20 totalmente funcional. Você pode expandir
.deps>npmpara ver o código importado. -
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:
-
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.
-
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 virtual3 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 virtualA 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
tonão pode ser igual aaddress(this), o endereço do próprio contrato ERC-20. - O endereço
tonã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>.codee<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:
-
Congelar e descongelar contas. Isso pode ser útil, por exemplo, quando uma conta pode ser comprometida.
-
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:
- Contratos
Ownable(opens in a new tab) têm um único proprietário. Funções que têm o modificador (opens in a new tab)onlyOwnersó 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 normalmente idênticos. - Contratos
AccessControl(opens in a new tab) têm controle de acesso baseado em função (RBAC) (opens in a new tab).
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 descongeladas2 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 public3 onlyOwnerFunçõ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 } // freezeAccountSe a conta já estiver congelada, a transação será revertida. Caso contrário, congele-a e emita um evento com
emit. -
Altere o
_beforeTokenTransferpara 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 dest4 )5 public6 onlyOwner7 {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