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:
Abra o IDE Remix.
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 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:
Selecione ERC20.
Digite estas configurações:
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.
Agora temos um contrato ERC-20 totalmente funcional. Você pode expandir o
.deps > npm
para ver o código importado.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:
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.
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);
}
Algumas partes dessa função podem ser novas se você não estiver muito familiarizado com o Solidity:
internal virtual
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)
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);
Essa linha chama a função _beforeTokenTransfer
do contrato ou dos contratosdos quais a herdamos. Nesse caso, é apenas o ERC20, o Ownable
nã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 aaddress(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");
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)
}
É 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");
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:
Contas Congeladas e descongeladas. Isso pode ser útil, por exemplo, quando uma conta pode estar comprometida.
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;
- 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);
- 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
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
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");
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);
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);
}
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.
Top comments (0)