Passo a passo do contrato ERC-20
Introdução
Um dos usos mais comuns do Ethereum é para um grupo criar um token negociável, em certo sentido, sua própria moeda. Esses tokens normalmente seguem um padrão, o ERC-20. Esse padrão torna possível escrever ferramentas, como pools de liquidez e carteiras, que funcionam com todos os tokens ERC-20. Neste artigo, analisaremos a implementação ERC20 em Solidity da OpenZeppelin (opens in a new tab), bem como a definição da interface (opens in a new tab).
Este é um código-fonte anotado. Se você quiser implementar o ERC-20, leia este tutorial (opens in a new tab).
A Interface
O propósito de um padrão como o ERC-20 é permitir muitas implementações de tokens que sejam interoperáveis entre aplicativos, como carteiras e exchanges descentralizadas. Para alcançar isso, criamos uma interface (opens in a new tab). Qualquer código que precise usar o contrato do token pode usar as mesmas definições na interface e ser compatível com todos os contratos de token que a utilizam, seja uma carteira como a MetaMask, um aplicativo descentralizado (dapp) como o etherscan.io, ou um contrato diferente, como um pool de liquidez.
Se você é um programador experiente, provavelmente se lembra de ter visto construções semelhantes em Java (opens in a new tab) ou até mesmo em arquivos de cabeçalho C (opens in a new tab).
Esta é uma definição da Interface ERC-20 (opens in a new tab) da OpenZeppelin. É uma tradução do padrão legível por humanos (opens in a new tab) para código Solidity. Claro, a interface em si não define como fazer nada. Isso é explicado no código-fonte do contrato abaixo.
// SPDX-License-Identifier: MIT
Os arquivos Solidity devem incluir um identificador de licença. Você pode ver a lista de licenças aqui (opens in a new tab). Se você precisar de uma licença diferente, basta explicá-la nos comentários.
pragma solidity >=0.6.0 <0.8.0;
A linguagem Solidity ainda está evoluindo rapidamente, e novas versões podem não ser compatíveis com códigos antigos (veja aqui (opens in a new tab)). Portanto, é uma boa ideia especificar não apenas uma versão mínima da linguagem, mas também uma versão máxima, a mais recente com a qual você testou o código.
/**
* @dev Interface do padrão ERC-20 conforme definido na EIP.
*/
O @dev no comentário faz parte do formato NatSpec (opens in a new tab), usado para produzir documentação a partir do código-fonte.
interface IERC20 {
Por convenção, os nomes das interfaces começam com I.
/**
* @dev Retorna a quantidade de tokens em existência.
*/
function totalSupply() external view returns (uint256);
Esta função é external, o que significa que ela só pode ser chamada de fora do contrato (opens in a new tab). Ela retorna o suprimento total de tokens no contrato. Esse valor é retornado usando o tipo mais comum no Ethereum, 256 bits sem sinal (256 bits é o tamanho de palavra nativo da EVM). Esta função também é uma view, o que significa que ela não altera o estado, então pode ser executada em um único nó em vez de fazer com que todos os nós da blockchain a executem. Esse tipo de função não gera uma transação e não custa gás.
Nota: Na teoria, pode parecer que o criador de um contrato poderia trapacear retornando um suprimento total menor que o valor real, fazendo com que cada token pareça mais valioso do que realmente é. No entanto, esse medo ignora a verdadeira natureza da blockchain. Tudo o que acontece na blockchain pode ser verificado por todos os nós. Para alcançar isso, o código de linguagem de máquina e o armazenamento de cada contrato estão disponíveis em todos os nós. Embora você não seja obrigado a publicar o código Solidity do seu contrato, ninguém o levaria a sério a menos que você publique o código-fonte e a versão do Solidity com a qual ele foi compilado, para que possa ser verificado em relação ao código de linguagem de máquina que você forneceu. Por exemplo, veja este contrato (opens in a new tab).
/**
* @dev Retorna a quantidade de tokens pertencentes à `account`.
*/
function balanceOf(address account) external view returns (uint256);
Como o nome diz, balanceOf retorna o saldo de uma conta. As contas Ethereum são identificadas em Solidity usando o tipo address, que contém 160 bits. Ela também é external e view.
/**
* @dev Move `amount` tokens da conta do chamador para `recipient`.
*
* Retorna um valor booleano indicando se a operação foi bem-sucedida.
*
* Emite um evento {Transfer}.
*/
function transfer(address recipient, uint256 amount) external returns (bool);
A função transfer transfere tokens do chamador para um endereço diferente. Isso envolve uma mudança de estado, então não é uma view. Quando um usuário chama essa função, ela cria uma transação e custa gás. Ela também emite um evento, Transfer, para informar a todos na blockchain sobre o evento.
A função tem dois tipos de saída para dois tipos diferentes de chamadores:
- Usuários que chamam a função diretamente de uma interface de usuário. Normalmente, o usuário envia uma transação e não espera por uma resposta, o que pode levar um tempo indefinido. O usuário pode ver o que aconteceu procurando pelo recibo da transação (que é identificado pelo hash da transação) ou procurando pelo evento
Transfer. - Outros contratos, que chamam a função como parte de uma transação geral. Esses contratos obtêm o resultado imediatamente, porque são executados na mesma transação, para que possam usar o valor de retorno da função.
O mesmo tipo de saída é criado pelas outras funções que alteram o estado do contrato.
As permissões permitem que uma conta gaste alguns tokens que pertencem a um proprietário diferente. Isso é útil, por exemplo, para contratos que atuam como vendedores. Os contratos não podem monitorar eventos, então se um comprador transferisse tokens diretamente para o contrato do vendedor, esse contrato não saberia que foi pago. Em vez disso, o comprador permite que o contrato do vendedor gaste uma certa quantia, e o vendedor transfere essa quantia. Isso é feito por meio de uma função que o contrato do vendedor chama, para que o contrato do vendedor possa saber se foi bem-sucedido.
/**
* @dev Retorna o número restante de tokens que `spender` terá
* permissão para gastar em nome de `owner` por meio de {transferFrom}. Isso é
* zero por padrão.
*
* Este valor muda quando {approve} ou {transferFrom} são chamados.
*/
function allowance(address owner, address spender) external view returns (uint256);
A função allowance permite que qualquer pessoa consulte para ver qual é a permissão que um endereço (owner) permite que outro endereço (spender) gaste.
/**
* @dev Define `amount` como a permissão de `spender` sobre os tokens do chamador.
*
* Retorna um valor booleano indicando se a operação foi bem-sucedida.
*
* IMPORTANTE: Cuidado, pois alterar uma permissão com este método traz o risco
* de que alguém possa usar tanto a permissão antiga quanto a nova devido a uma
* ordenação de transação infeliz. Uma solução possível para mitigar essa condição
* de corrida é primeiro reduzir a permissão do gastador para 0 e definir o
* valor desejado depois:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emite um evento {Approval}.
*/
function approve(address spender, uint256 amount) external returns (bool);
A função approve cria uma permissão. Certifique-se de ler a mensagem sobre como ela pode ser abusada. No Ethereum, você controla a ordem de suas próprias transações, mas não pode controlar a ordem em que as transações de outras pessoas serão executadas, a menos que você não envie sua própria transação até ver que a transação do outro lado aconteceu.
/**
* @dev Move `amount` tokens de `sender` para `recipient` usando o
* mecanismo de permissão. `amount` é então deduzido da
* permissão do chamador.
*
* Retorna um valor booleano indicando se a operação foi bem-sucedida.
*
* Emite um evento {Transfer}.
*/
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
Finalmente, transferFrom é usado pelo gastador para realmente gastar a permissão.
/**
* @dev Emitido quando `value` tokens são movidos de uma conta (`from`) para
* outra (`to`).
*
* Note que `value` pode ser zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitido quando a permissão de um `spender` para um `owner` é definida por
* uma chamada para {approve}. `value` é a nova permissão.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
}
Esses eventos são emitidos quando o estado do contrato ERC-20 muda.
O Contrato Real
Este é o contrato real que implementa o padrão ERC-20, retirado daqui (opens in a new tab). Ele não deve ser usado como está, mas você pode herdar (opens in a new tab) dele para estendê-lo a algo utilizável.
// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;
Declarações de Importação
Além das definições de interface acima, a definição do contrato importa dois outros arquivos:
import "../../GSN/Context.sol";
import "./IERC20.sol";
import "../../math/SafeMath.sol";
GSN/Context.solsão as definições necessárias para usar o OpenGSN (opens in a new tab), um sistema que permite que usuários sem ether usem a blockchain. Observe que esta é uma versão antiga; se você quiser se integrar ao OpenGSN, use este tutorial (opens in a new tab).- A biblioteca SafeMath (opens in a new tab), que previne overflows/underflows aritméticos para versões do Solidity <0.8.0. No Solidity ≥0.8.0, as operações aritméticas revertem automaticamente em overflow/underflow, tornando o SafeMath desnecessário. Este contrato usa o SafeMath para compatibilidade com versões anteriores de compiladores.
Este comentário explica o propósito do contrato.
/**
* @dev Implementação da interface {IERC20}.
*
* Esta implementação é agnóstica à forma como os tokens são criados. Isso significa
* que um mecanismo de fornecimento deve ser adicionado em um contrato derivado usando {_mint}.
* Para um mecanismo genérico, veja {ERC20PresetMinterPauser}.
*
* DICA: Para um artigo detalhado, veja nosso guia
* https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
* to implement supply mechanisms].
*
* Seguimos as diretrizes gerais da OpenZeppelin: as funções revertem em vez
* de retornar `false` em caso de falha. Esse comportamento é, no entanto, convencional
* e não entra em conflito com as expectativas das aplicações ERC-20.
*
* Adicionalmente, um evento {Approval} é emitido em chamadas para {transferFrom}.
* Isso permite que as aplicações reconstruam a permissão para todas as contas apenas
* ouvindo os referidos eventos. Outras implementações da EIP podem não emitir
* esses eventos, pois não é exigido pela especificação.
*
* Por fim, as funções não padrão {decreaseAllowance} e {increaseAllowance}
* foram adicionadas para mitigar os problemas bem conhecidos em torno da definição de
* permissões. Veja {IERC20-approve}.
*/
Definição do Contrato
contract ERC20 is Context, IERC20 {
Esta linha especifica a herança, neste caso de IERC20 acima e Context, para o OpenGSN.
using SafeMath for uint256;
Esta linha anexa a biblioteca SafeMath ao tipo uint256. Você pode encontrar esta biblioteca aqui (opens in a new tab).
Definições de Variáveis
Essas definições especificam as variáveis de estado do contrato. Essas variáveis são declaradas como private, mas isso significa apenas que outros contratos na blockchain não podem lê-las. Não há segredos na blockchain, o software em cada nó tem o estado de cada contrato em cada bloco. Por convenção, as variáveis de estado são nomeadas com _<something>.
As duas primeiras variáveis são mapeamentos (opens in a new tab), o que significa que se comportam de forma semelhante a arrays associativos (opens in a new tab), exceto que as chaves são valores numéricos. O armazenamento é alocado apenas para entradas que têm valores diferentes do padrão (zero).
mapping (address => uint256) private _balances;
O primeiro mapeamento, _balances, são os endereços e seus respectivos saldos deste token. Para acessar o saldo, use esta sintaxe: _balances[<address>].
mapping (address => mapping (address => uint256)) private _allowances;
Esta variável, _allowances, armazena as permissões explicadas anteriormente. O primeiro índice é o proprietário dos tokens, e o segundo é o contrato com a permissão. Para acessar a quantia que o endereço A pode gastar da conta do endereço B, use _allowances[B][A].
uint256 private _totalSupply;
Como o nome sugere, esta variável rastreia o suprimento total de tokens.
string private _name;
string private _symbol;
uint8 private _decimals;
Essas três variáveis são usadas para melhorar a legibilidade. As duas primeiras são autoexplicativas, mas _decimals não é.
Por um lado, o Ethereum não tem variáveis de ponto flutuante ou fracionárias. Por outro lado, os humanos gostam de poder dividir tokens. Uma das razões pelas quais as pessoas adotaram o ouro como moeda foi que era difícil dar o troco quando alguém queria comprar o equivalente a um pato em vaca.
A solução é rastrear números inteiros, mas contar, em vez do token real, um token fracionário que é quase sem valor. No caso do ether, o token fracionário é chamado de Wei, e 10^18 Wei é igual a um ETH. No momento da escrita, 10.000.000.000.000 Wei equivalem a aproximadamente um centavo de dólar ou euro.
Os aplicativos precisam saber como exibir o saldo do token. Se um usuário tem 3.141.000.000.000.000.000 Wei, isso é 3,14 ETH? 31,41 ETH? 3.141 ETH? No caso do ether, é definido 10^18 Wei para o ETH, mas para o seu token você pode selecionar um valor diferente. Se dividir o token não fizer sentido, você pode usar um valor de _decimals igual a zero. Se você quiser usar o mesmo padrão do ETH, use o valor 18.
O Construtor
/**
* @dev Define os valores para {name} e {symbol}, inicializa {decimals} com
* um valor padrão de 18.
*
* Para selecionar um valor diferente para {decimals}, use {_setupDecimals}.
*
* Todos esses três valores são imutáveis: eles só podem ser definidos uma vez durante
* a construção.
*/
constructor (string memory name_, string memory symbol_) public {
// Na Solidity ≥0.7.0, 'public' é implícito e pode ser omitido.
_name = name_;
_symbol = symbol_;
_decimals = 18;
}
O construtor é chamado quando o contrato é criado pela primeira vez. Por convenção, os parâmetros da função são nomeados com <something>_.
Funções de Interface de Usuário
/**
* @dev Retorna o nome do token.
*/
function name() public view returns (string memory) {
return _name;
}
/**
* @dev Retorna o símbolo do token, geralmente uma versão mais curta do
* nome.
*/
function symbol() public view returns (string memory) {
return _symbol;
}
/**
* @dev Retorna o número de casas decimais usadas para obter sua representação para o usuário.
* Por exemplo, se `decimals` for igual a `2`, um saldo de `505` tokens deve
* ser exibido para um usuário como `5,05` (`505 / 10 ** 2`).
*
* Tokens geralmente optam por um valor de 18, imitando a relação entre
* ether e Wei. Este é o valor que o {ERC20} usa, a menos que {_setupDecimals} seja
* chamado.
*
* NOTA: Esta informação é usada apenas para fins de _exibição_: ela de
* forma alguma afeta qualquer aritmética do contrato, incluindo
* {IERC20-balanceOf} e {IERC20-transfer}.
*/
function decimals() public view returns (uint8) {
return _decimals;
}
Essas funções, name, symbol e decimals, ajudam as interfaces de usuário a conhecer seu contrato para que possam exibi-lo corretamente.
O tipo de retorno é string memory, o que significa retornar uma string que é armazenada na memória. Variáveis, como strings, podem ser armazenadas em três locais:
| Tempo de vida | Acesso do Contrato | Custo de Gás | |
|---|---|---|---|
| Memória | Chamada de função | Leitura/Escrita | Dezenas ou centenas (maior para locais mais altos) |
| Dados de chamada | Chamada de função | Somente Leitura | Não pode ser usado como um tipo de retorno, apenas como um tipo de parâmetro de função |
| Armazenamento | Até ser alterado | Leitura/Escrita | Alto (800 para leitura, 20k para escrita) |
Neste caso, memory é a melhor escolha.
Ler Informações do Token
Estas são funções que fornecem informações sobre o token, seja o suprimento total ou o saldo de uma conta.
/**
* @dev Veja {IERC20-totalSupply}.
*/
function totalSupply() public view override returns (uint256) {
return _totalSupply;
}
A função totalSupply retorna o suprimento total de tokens.
/**
* @dev Veja {IERC20-balanceOf}.
*/
function balanceOf(address account) public view override returns (uint256) {
return _balances[account];
}
Lê o saldo de uma conta. Observe que qualquer pessoa tem permissão para obter o saldo da conta de qualquer outra pessoa. Não faz sentido tentar esconder essa informação, porque ela está disponível em todos os nós de qualquer maneira. Não há segredos na blockchain.
Transferir Tokens
/**
* @dev Veja {IERC20-transfer}.
*
* Requisitos:
*
* - `recipient` não pode ser o endereço zero.
* - o chamador deve ter um saldo de pelo menos `amount`.
*/
function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
A função transfer é chamada para transferir tokens da conta do remetente para uma diferente. Observe que, embora retorne um valor booleano, esse valor é sempre true. Se a transferência falhar, o contrato reverte a chamada.
_transfer(_msgSender(), recipient, amount);
return true;
}
A função _transfer faz o trabalho real. É uma função privada que só pode ser chamada por outras funções do contrato. Por convenção, as funções privadas são nomeadas com _<something>, assim como as variáveis de estado.
Normalmente, em Solidity, usamos msg.sender para o remetente da mensagem. No entanto, isso quebra o OpenGSN (opens in a new tab). Se quisermos permitir transações sem ether com nosso token, precisamos usar _msgSender(). Ele retorna msg.sender para transações normais, mas para as sem ether retorna o signatário original e não o contrato que retransmitiu a mensagem.
Funções de Permissão
Estas são as funções que implementam a funcionalidade de permissão: allowance, approve, transferFrom e _approve. Além disso, a implementação da OpenZeppelin vai além do padrão básico para incluir alguns recursos que melhoram a segurança: increaseAllowance e decreaseAllowance.
A função allowance
/**
* @dev Veja {IERC20-allowance}.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
A função allowance permite que todos verifiquem qualquer permissão.
A função approve
/**
* @dev Veja {IERC20-approve}.
*
* Requisitos:
*
* - `spender` não pode ser o endereço zero.
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
Esta função é chamada para criar uma permissão. É semelhante à função transfer acima:
- A função apenas chama uma função interna (neste caso,
_approve) que faz o trabalho real. - A função retorna
true(se for bem-sucedida) ou reverte (se não for).
_approve(_msgSender(), spender, amount);
return true;
}
Usamos funções internas para minimizar o número de lugares onde as mudanças de estado acontecem. Qualquer função que altere o estado é um risco potencial de segurança que precisa ser auditado quanto à segurança. Dessa forma, temos menos chances de errar.
A função transferFrom
Esta é a função que um gastador chama para gastar uma permissão. Isso requer duas operações: transferir a quantia que está sendo gasta e reduzir a permissão por essa quantia.
/**
* @dev Veja {IERC20-transferFrom}.
*
* Emite um evento {Approval} indicando a permissão atualizada. Isso não é
* exigido pela EIP. Veja a nota no início do {ERC20}.
*
* Requisitos:
*
* - `sender` e `recipient` não podem ser o endereço zero.
* - `sender` deve ter um saldo de pelo menos `amount`.
* - o chamador deve ter permissão para os tokens de ``sender`` de pelo menos
* `amount`.
*/
function transferFrom(address sender, address recipient, uint256 amount) public virtual
override returns (bool) {
_transfer(sender, recipient, amount);
A chamada da função a.sub(b, "message") faz duas coisas. Primeiro, ela calcula a-b, que é a nova permissão. Segundo, ela verifica se esse resultado não é negativo. Se for negativo, a chamada reverte com a mensagem fornecida. Observe que quando uma chamada reverte, qualquer processamento feito anteriormente durante essa chamada é ignorado, então não precisamos desfazer o _transfer.
_approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
"ERC20: transfer amount exceeds allowance"));
return true;
}
Adições de segurança da OpenZeppelin
É perigoso definir uma permissão diferente de zero para outro valor diferente de zero, porque você só controla a ordem de suas próprias transações, não a de mais ninguém. Imagine que você tem dois usuários, Alice, que é ingênua, e Bill, que é desonesto. Alice quer um serviço de Bill, que ela acha que custa cinco tokens - então ela dá a Bill uma permissão de cinco tokens.
Então algo muda e o preço de Bill sobe para dez tokens. Alice, que ainda quer o serviço, envia uma transação que define a permissão de Bill para dez. No momento em que Bill vê essa nova transação no pool de transações, ele envia uma transação que gasta os cinco tokens de Alice e tem um preço do gás muito mais alto para que seja minerada mais rápido. Dessa forma, Bill pode gastar primeiro cinco tokens e depois, assim que a nova permissão de Alice for minerada, gastar mais dez por um preço total de quinze tokens, mais do que Alice pretendia autorizar. Essa técnica é chamada de front-running (opens in a new tab)
| Transação de Alice | Nonce de Alice | Transação de Bill | Nonce de Bill | Permissão de Bill | Renda Total de Bill de Alice |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| approve(Bill, 10) | 11 | 10 | 5 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 |
Para evitar esse problema, essas duas funções (increaseAllowance e decreaseAllowance) permitem que você modifique a permissão por uma quantia específica. Então, se Bill já tivesse gasto cinco tokens, ele só poderia gastar mais cinco. Dependendo do momento, há duas maneiras de isso funcionar, ambas terminando com Bill recebendo apenas dez tokens:
A:
| Transação de Alice | Nonce de Alice | Transação de Bill | Nonce de Bill | Permissão de Bill | Renda Total de Bill de Alice |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| increaseAllowance(Bill, 5) | 11 | 0+5 = 5 | 5 | ||
| transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 |
B:
| Transação de Alice | Nonce de Alice | Transação de Bill | Nonce de Bill | Permissão de Bill | Renda Total de Bill de Alice |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
/**
* @dev Aumenta atomicamente a permissão concedida a `spender` pelo chamador.
*
* Esta é uma alternativa para {approve} que pode ser usada como mitigação para
* problemas descritos em {IERC20-approve}.
*
* Emite um evento {Approval} indicando a permissão atualizada.
*
* Requisitos:
*
* - `spender` não pode ser o endereço zero.
*/
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
_approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));
return true;
}
A função a.add(b) é uma adição segura. No caso improvável de que a+b>=2^256, ela não dá a volta (wrap around) da maneira que a adição normal faz.
/**
* @dev Diminui atomicamente a permissão concedida a `spender` pelo chamador.
*
* Esta é uma alternativa para {approve} que pode ser usada como mitigação para
* problemas descritos em {IERC20-approve}.
*
* Emite um evento {Approval} indicando a permissão atualizada.
*
* Requisitos:
*
* - `spender` não pode ser o endereço zero.
* - `spender` deve ter permissão para o chamador de pelo menos
* `subtractedValue`.
*/
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
_approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,
"ERC20: decreased allowance below zero"));
return true;
}
Funções que Modificam Informações do Token
Estas são as quatro funções que fazem o trabalho real: _transfer, _mint, _burn e _approve.
A função _transfer
/**
* @dev Move `amount` tokens de `sender` para `recipient`.
*
* Esta função interna é equivalente a {transfer}, e pode ser usada para
* ex., implementar taxas automáticas de token, mecanismos de slashing, etc.
*
* Emite um evento {Transfer}.
*
* Requisitos:
*
* - `sender` não pode ser o endereço zero.
* - `recipient` não pode ser o endereço zero.
* - `sender` deve ter um saldo de pelo menos `amount`.
*/
function _transfer(address sender, address recipient, uint256 amount) internal virtual {
Esta função, _transfer, transfere tokens de uma conta para outra. Ela é chamada tanto por transfer (para transferências da própria conta do remetente) quanto por transferFrom (para usar permissões para transferir da conta de outra pessoa).
require(sender != address(0), "ERC20: transfer from the zero address");
require(recipient != address(0), "ERC20: transfer to the zero address");
Ninguém realmente possui o endereço zero no Ethereum (ou seja, ninguém conhece uma chave privada cuja chave pública correspondente seja transformada no endereço zero). Quando as pessoas usam esse endereço, geralmente é um bug de software - então falhamos se o endereço zero for usado como remetente ou destinatário.
_beforeTokenTransfer(sender, recipient, amount);
Existem duas maneiras de usar este contrato:
- Usá-lo como um modelo para o seu próprio código
- Herdar dele (opens in a new tab) e substituir apenas as funções que você precisa modificar
O segundo método é muito melhor porque o código ERC-20 da OpenZeppelin já foi auditado e provou ser seguro. Quando você usa herança, fica claro quais são as funções que você modifica, e para confiar no seu contrato, as pessoas só precisam auditar essas funções específicas.
Muitas vezes é útil executar uma função cada vez que os tokens mudam de mãos. No entanto, _transfer é uma função muito importante e é possível escrevê-la de forma insegura (veja abaixo), então é melhor não substituí-la. A solução é _beforeTokenTransfer, uma função de gancho (hook) (opens in a new tab). Você pode substituir esta função, e ela será chamada em cada transferência.
_balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
_balances[recipient] = _balances[recipient].add(amount);
Estas são as linhas que realmente fazem a transferência. Observe que não há nada entre elas, e que subtraímos a quantia transferida do remetente antes de adicioná-la ao destinatário. Isso é importante porque se houvesse uma chamada para um contrato diferente no meio, ela poderia ter sido usada para trapacear este contrato. Dessa forma, a transferência é atômica, nada pode acontecer no meio dela.
emit Transfer(sender, recipient, amount);
}
Finalmente, emita um evento Transfer. Os eventos não são acessíveis a contratos inteligentes, mas o código executado fora da blockchain pode escutar eventos e reagir a eles. Por exemplo, uma carteira pode rastrear quando o proprietário recebe mais tokens.
As funções _mint e _burn
Essas duas funções (_mint e _burn) modificam o suprimento total de tokens. Elas são internas e não há nenhuma função que as chame neste contrato, então elas só são úteis se você herdar do contrato e adicionar sua própria lógica para decidir sob quais condições cunhar novos tokens ou queimar os existentes.
NOTA: Cada token ERC-20 tem sua própria lógica de negócios que dita o gerenciamento do token. Por exemplo, um contrato de suprimento fixo pode chamar _mint apenas no construtor e nunca chamar _burn. Um contrato que vende tokens chamará _mint quando for pago e, presumivelmente, chamará _burn em algum momento para evitar uma inflação descontrolada.
/** @dev Cria `amount` tokens e os atribui a `account`, aumentando
* o fornecimento total.
*
* Emite um evento {Transfer} com `from` definido como o endereço zero.
*
* Requisitos:
*
* - `to` não pode ser o endereço zero.
*/
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply = _totalSupply.add(amount);
_balances[account] = _balances[account].add(amount);
emit Transfer(address(0), account, amount);
}
Certifique-se de atualizar _totalSupply quando o número total de tokens mudar.
/**
* @dev Destrói `amount` tokens de `account`, reduzindo o
* fornecimento total.
*
* Emite um evento {Transfer} com `to` definido como o endereço zero.
*
* Requisitos:
*
* - `account` não pode ser o endereço zero.
* - `account` deve ter pelo menos `amount` tokens.
*/
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
_balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");
_totalSupply = _totalSupply.sub(amount);
emit Transfer(account, address(0), amount);
}
A função _burn é quase idêntica a _mint, exceto que vai na outra direção.
A função _approve
Esta é a função que realmente especifica as permissões. Observe que ela permite que um proprietário especifique uma permissão que é maior que o saldo atual do proprietário. Isso é aceitável porque o saldo é verificado no momento da transferência, quando pode ser diferente do saldo de quando a permissão foi criada.
/**
* @dev Define `amount` como a permissão de `spender` sobre os tokens de `owner`.
*
* Esta função interna é equivalente a `approve`, e pode ser usada para
* ex., definir permissões automáticas para certos subsistemas, etc.
*
* Emite um evento {Approval}.
*
* Requisitos:
*
* - `owner` não pode ser o endereço zero.
* - `spender` não pode ser o endereço zero.
*/
function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
Emita um evento Approval. Dependendo de como o aplicativo é escrito, o contrato do gastador pode ser informado sobre a aprovação pelo proprietário ou por um servidor que escuta esses eventos.
emit Approval(owner, spender, amount);
}
Modificar a Variável Decimals
/**
* @dev Define {decimals} para um valor diferente do padrão de 18.
*
* AVISO: Esta função deve ser chamada apenas do construtor. A maioria das
* aplicações que interagem com contratos de token não esperará
* que {decimals} mude, e podem funcionar incorretamente se isso acontecer.
*/
function _setupDecimals(uint8 decimals_) internal {
_decimals = decimals_;
}
Esta função modifica a variável _decimals, que é usada para dizer às interfaces de usuário como interpretar a quantia. Você deve chamá-la a partir do construtor. Seria desonesto chamá-la em qualquer ponto subsequente, e os aplicativos não são projetados para lidar com isso.
Ganchos (Hooks)
/**
* @dev Hook que é chamado antes de qualquer transferência de tokens. Isso inclui
* cunhagem (minting) e queima (burning).
*
* Condições de chamada:
*
* - quando `from` e `to` são ambos não zero, `amount` dos tokens de ``from``
* serão transferidos para `to`.
* - quando `from` é zero, `amount` tokens serão cunhados para `to`.
* - quando `to` é zero, `amount` dos tokens de ``from`` serão queimados.
* - `from` e `to` nunca são ambos zero.
*
* Para saber mais sobre hooks, acesse xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
}
Esta é a função de gancho a ser chamada durante as transferências. Ela está vazia aqui, mas se você precisar que ela faça algo, basta substituí-la.
Conclusão
Para revisão, aqui estão algumas das ideias mais importantes neste contrato (na minha opinião, a sua provavelmente irá variar):
- Não há segredos na blockchain. Qualquer informação que um contrato inteligente possa acessar está disponível para o mundo todo.
- Você pode controlar a ordem de suas próprias transações, mas não quando as transações de outras pessoas acontecem. Essa é a razão pela qual alterar uma permissão pode ser perigoso, porque permite que o gastador gaste a soma de ambas as permissões.
- Valores do tipo
uint256dão a volta (wrap around). Em outras palavras, 0-1=2^256-1. Se esse não for o comportamento desejado, você deve verificar isso (ou usar a biblioteca SafeMath que faz isso por você). Observe que isso mudou no Solidity 0.8.0 (opens in a new tab). - Faça todas as mudanças de estado de um tipo específico em um lugar específico, porque isso facilita a auditoria. Essa é a razão pela qual temos, por exemplo,
_approve, que é chamado porapprove,transferFrom,increaseAllowanceedecreaseAllowance - As mudanças de estado devem ser atômicas, sem nenhuma outra ação no meio delas (como você pode ver em
_transfer). Isso ocorre porque durante a mudança de estado você tem um estado inconsistente. Por exemplo, entre o momento em que você deduz do saldo do remetente e o momento em que você adiciona ao saldo do destinatário, há menos tokens em existência do que deveria haver. Isso poderia ser potencialmente abusado se houver operações entre eles, especialmente chamadas para um contrato diferente.
Agora que você viu como o contrato ERC-20 da OpenZeppelin é escrito, e especialmente como ele é tornado mais seguro, vá e escreva seus próprios contratos e aplicativos seguros.
