Passo a passo do contrato ERC-20
Introdução
Um dos usos mais comuns do Ethereum é a criação por um grupo de pessoas de um token negociável que, de certa forma, criam sua própria moeda. Esses tokens geralmente seguem um padrão, o ERC-20. Esse padrão possibilita 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 objetivo de um padrão como o ERC-20 é permitir que muitas implementações de tokens sejam interoperáveis entre aplicativos, como carteiras e corretoras descentralizadas. Para conseguir isso, criamos uma interface (opens in a new tab). Qualquer código que precise usar o contrato de 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 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 o código Solidity. Claro, a interface em si não define como fazer nada. Isso é explicado no código-fonte do contrato abaixo.
1// SPDX-License-Identifier: MITOs 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.
1pragma 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.
1/**2 * @dev Interface do padrão ERC20, conforme definido na EIP.3 */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.
1interface IERC20 {Por convenção, os nomes de interface começam com I.
1 /**2 * @dev Retorna a quantidade de tokens existentes.3 */4 function totalSupply() external view returns (uint256);Esta função é external, o que significa que só pode ser chamada de fora do contrato (opens in a new tab).
Ela retorna o fornecimento total de tokens no contrato. Este valor é retornado usando o tipo mais comum no Ethereum, 256 bits sem sinal (256 bits é o
tamanho nativo da palavra da EVM). Essa função também é uma view, o que significa que ela não altera o estado, portanto, 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.
Observação: em teoria, pode parecer que o criador de um contrato poderia trapacear retornando um fornecimento 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 conseguir 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 com o código de linguagem de máquina que você forneceu. Por exemplo, veja este contrato (opens in a new tab).
1 /**2 * @dev Retorna a quantidade de tokens de propriedade de `account`.3 */4 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 no Solidity usando o tipo address, que contém 160 bits.
Ela também é external e view.
1 /**2 * @dev Move a quantidade `amount` de tokens da conta do chamador para o `recipient`.3 *4 * Retorna um valor booleano que indica se a operação foi bem-sucedida.5 *6 * Emite um evento {Transfer}.7 */8 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, que pode levar um tempo indefinido. O usuário pode ver o que aconteceu
procurando o 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 (allowances) 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; portanto, se um comprador transferisse tokens para o contrato do vendedor diretamente, 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.
1 /**2 * @dev Retorna o número restante de tokens que o `spender` terá3 * permissão para gastar em nome do `owner` por meio de {transferFrom}. O padrão4 * é zero.5 *6 * Esse valor muda quando {approve} ou {transferFrom} são chamados.7 */8 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.
1 /**2 * @dev Define `amount` como a permissão de `spender` sobre os tokens do chamador.3 *4 * Retorna um valor booleano que indica se a operação foi bem-sucedida.5 *6 * IMPORTANTE: Cuidado, pois alterar uma permissão com este método traz o risco7 * de que alguém possa usar tanto a permissão antiga quanto a nova por meio de uma8 * ordem de transação infeliz. Uma solução possível para mitigar essa condição9 * de corrida é primeiro reduzir a permissão do gastador para 0 e depois definir o10 * valor desejado:11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472912 *13 * Emite um evento {Approval}.14 */15 function approve(address spender, uint256 amount) external returns (bool);Exibir tudoA função approve cria uma permissão. Certifique-se de ler a mensagem sobre
como ela pode ser usada indevidamente. 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 da outra parte aconteceu.
1 /**2 * @dev Move `amount` tokens de `sender` para `recipient` usando o3 * mecanismo de permissão. `amount` é então deduzido da4 * permissão do chamador.5 *6 * Retorna um valor booleano que indica se a operação foi bem-sucedida.7 *8 * Emite um evento {Transfer}.9 */10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);Exibir tudoFinalmente, transferFrom é usado pelo gastador para realmente gastar a permissão.
12 /**3 * @dev Emitido quando `value` tokens são movidos de uma conta (`from`) para4 * outra (`to`).5 *6 * Note que `value` pode ser zero.7 */8 event Transfer(address indexed from, address indexed to, uint256 value);910 /**11 * @dev Emitido quando a permissão de um `spender` para um `owner` é definida por12 * uma chamada para {approve}. `value` é a nova permissão.13 */14 event Approval(address indexed owner, address indexed spender, uint256 value);15}Exibir tudoEsses 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 se destina a ser usado como está, mas você pode herdar (opens in a new tab) dele para estendê-lo para algo utilizável.
1// SPDX-License-Identifier: MIT2pragma 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:
12import "../../GSN/Context.sol";3import "./IERC20.sol";4import "../../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 integrar com o OpenGSN, use este tutorial (opens in a new tab).- A biblioteca SafeMath (opens in a new tab), que impede estouros/subfluxos aritméticos para versões do Solidity <0.8.0. No Solidity ≥0.8.0, as operações aritméticas revertem automaticamente em caso de estouro/subfluxo, tornando o SafeMath desnecessário. Este contrato usa o SafeMath para compatibilidade com versões anteriores de compiladores mais antigos.
Este comentário explica o propósito do contrato.
1/**2 * @dev Implementação da interface {IERC20}.3 *4 * Esta implementação é agnóstica à forma como os tokens são criados. Isso significa5 * que um mecanismo de fornecimento deve ser adicionado em um contrato derivado usando {_mint}.6 * Para um mecanismo genérico, veja {ERC20PresetMinterPauser}.7 *8 * DICA: Para um artigo detalhado, consulte nosso guia9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[Como10 * implementar mecanismos de fornecimento].11 *12 * Seguimos as diretrizes gerais da OpenZeppelin: as funções são revertidas em vez13 * de retornar `false` em caso de falha. Esse comportamento, no entanto, é convencional14 * e não entra em conflito com as expectativas dos aplicativos ERC20.15 *16 * Além disso, um evento {Approval} é emitido em chamadas para {transferFrom}.17 * Isso permite que os aplicativos reconstruam a permissão para todas as contas apenas18 * ouvindo esses eventos. Outras implementações da EIP podem não emitir19 * esses eventos, pois não é exigido pela especificação.20 *21 * Finalmente, as funções não padrão {decreaseAllowance} e {increaseAllowance}22 * foram adicionadas para mitigar os problemas conhecidos em torno da definição23 * de permissões. Veja {IERC20-approve}.24 */25Exibir tudoDefinição do Contrato
1contract ERC20 is Context, IERC20 {Esta linha especifica a herança, neste caso de IERC20 acima e Context, para OpenGSN.
12 using SafeMath for uint256;3Essa 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 _<algo>.
As duas primeiras variáveis são mapeamentos (opens in a new tab), , o que significa que elas se comportam de forma semelhante a matrizes associativas (opens in a new tab), , exceto que as chaves são valores numéricos. O armazenamento só é alocado para entradas que têm valores diferentes do padrão (zero).
1 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[<endereço>].
1 mapping (address => mapping (address => uint256)) private _allowances;Essa 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].
1 uint256 private _totalSupply;Como o nome sugere, essa variável acompanha o fornecimento total de tokens.
1 string private _name;2 string private _symbol;3 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 optaram pelo ouro como moeda foi porque era difícil dar troco quando alguém queria comprar o equivalente a um pato em uma vaca.
A solução é manter o controle de 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 em que este artigo foi escrito, 10.000.000.000.000 wei equivalem a aproximadamente um centavo de dólar americano ou euro.
Os aplicativos precisam saber como exibir o saldo do token. Se um usuário tiver 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 a divisão do token não fizer sentido, você pode usar um
valor _decimals de zero. Se você quiser usar o mesmo padrão do ETH, use o valor 18.
O construtor
1 /**2 * @dev Define os valores para {name} e {symbol}, inicializa {decimals} com3 * um valor padrão de 18.4 *5 * Para selecionar um valor diferente para {decimals}, use {_setupDecimals}.6 *7 * Todos esses três valores são imutáveis: eles só podem ser definidos uma vez durante8 * a construção.9 */10 constructor (string memory name_, string memory symbol_) public {11 // No Solidity ≥0.7.0, 'public' é implícito e pode ser omitido.1213 _name = name_;14 _symbol = symbol_;15 _decimals = 18;16 }Exibir tudoO construtor é chamado quando o contrato é criado pela primeira vez. Por convenção, os parâmetros da função são nomeados <algo>_.
Funções da Interface do Usuário
1 /**2 * @dev Retorna o nome do token.3 */4 function name() public view returns (string memory) {5 return _name;6 }78 /**9 * @dev Retorna o símbolo do token, geralmente uma versão mais curta do10 * nome.11 */12 function symbol() public view returns (string memory) {13 return _symbol;14 }1516 /**17 * @dev Retorna o número de casas decimais usadas para obter sua representação de usuário.18 * Por exemplo, se `decimals` for igual a `2`, um saldo de `505` tokens deve19 * ser exibido para um usuário como `5,05` (`505 / 10 ** 2`).20 *21 * Geralmente, os tokens optam por um valor de 18, imitando a relação entre22 * ether e wei. Este é o valor que {ERC20} usa, a menos que {_setupDecimals} seja23 * chamado.24 *25 * OBSERVAÇÃO: esta informação é usada apenas para fins de _exibição_: ela26 * não afeta de forma alguma a aritmética do contrato, incluindo27 * {IERC20-balanceOf} e {IERC20-transfer}.28 */29 function decimals() public view returns (uint8) {30 return _decimals;31 }Exibir tudoEssas funções, name, symbol e decimals ajudam as interfaces de usuário a saber sobre o 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 ao contrato | Custo de gás | |
|---|---|---|---|
| Memória | Chamada de função | Leitura/Escrita | Dezenas ou centenas (maior para locais mais altos) |
| Calldata | Chamada de função | Somente leitura | Não pode ser usado como um tipo de retorno, apenas um tipo de parâmetro de função |
| Armazenamento | Até ser alterado | Leitura/Escrita | Alto (800 para leitura, 20 mil 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 fornecimento total ou o saldo de uma conta.
1 /**2 * @dev Ver {IERC20-totalSupply}.3 */4 function totalSupply() public view override returns (uint256) {5 return _totalSupply;6 }A função totalSupply retorna o fornecimento total de tokens.
1 /**2 * @dev Ver {IERC20-balanceOf}.3 */4 function balanceOf(address account) public view override returns (uint256) {5 return _balances[account];6 }Leia o saldo de uma conta. Observe que qualquer um pode 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
1 /**2 * @dev Ver {IERC20-transfer}.3 *4 * Requisitos:5 *6 * - `recipient` não pode ser o endereço zero.7 * - o chamador deve ter um saldo de pelo menos `amount`.8 */9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {Exibir tudoA função transfer é chamada para transferir tokens da conta do remetente para uma diferente. Observe
que, embora retorne um valor booleano, esse valor é sempre verdadeiro. Se a transferência
falhar, o contrato reverte a chamada.
1 _transfer(_msgSender(), recipient, amount);2 return true;3 }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 como _<algo>, da mesma forma que as variáveis
de estado.
Normalmente no 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 assinante 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 do 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
1 /**2 * @dev Ver {IERC20-allowance}.3 */4 function allowance(address owner, address spender) public view virtual override returns (uint256) {5 return _allowances[owner][spender];6 }A função allowance permite que todos verifiquem qualquer permissão.
A função approve
1 /**2 * @dev Ver {IERC20-approve}.3 *4 * Requisitos:5 *6 * - `spender` não pode ser o endereço zero.7 */8 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 ou retorna
true(se for bem-sucedida) ou reverte (se não for).
1 _approve(_msgSender(), spender, amount);2 return true;3 }Usamos funções internas para minimizar o número de locais onde ocorrem mudanças de estado. Qualquer função que altere o estado é um risco potencial de segurança que precisa ser auditado para 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 o valor gasto e reduzir a permissão por esse valor.
1 /**2 * @dev Ver {IERC20-transferFrom}.3 *4 * Emite um evento {Approval} indicando a permissão atualizada. Isso não5 * é exigido pela EIP. Veja a nota no início de {ERC20}.6 *7 * Requisitos:8 *9 * - `sender` e `recipient` não podem ser o endereço zero.10 * - `sender` deve ter um saldo de pelo menos `amount`.11 * - o chamador deve ter permissão para os tokens de ``sender`` de pelo menos12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 override returns (bool) {16 _transfer(sender, recipient, amount);Exibir tudo
A chamada da função a.sub(b, "mensagem") faz duas coisas. Primeiro, calcula a-b, que é a nova permissão.
Segundo, 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 a _transfer.
1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,2 "ERC20: transfer amount exceeds allowance"));3 return true;4 }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ê tenha dois usuários, Alice, que é ingênua, e Bill, que é desonesto. Alice quer algum 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 de gás muito mais alto para que seja minerado mais rápido. Dessa forma, Bill pode gastar primeiro cinco tokens e, em seguida, uma vez que a nova permissão de Alice seja 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 vinda 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 um valor específico. Então, se Bill já gastou cinco tokens, ele só
conseguirá gastar mais cinco. Dependendo do tempo, existem 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 vinda 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 vinda de Alice |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
| transferFrom(Alice, Bill, 10) | 10.124 | 0 | 10 |
1 /**2 * @dev Aumenta atomicamente a permissão concedida ao `spender` pelo chamador.3 *4 * Esta é uma alternativa para {approve} que pode ser usada como uma mitigação para5 * os problemas descritos em {IERC20-approve}.6 *7 * Emite um evento {Approval} indicando a permissão atualizada.8 *9 * Requisitos:10 *11 * - `spender` não pode ser o endereço zero.12 */13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));15 return true;16 }Exibir tudoA função a.add(b) é uma adição segura. No caso improvável de a+b>=2^256, ele não dá a volta
como a adição normal faz.
12 /**3 * @dev Diminui atomicamente a permissão concedida ao `spender` pelo chamador.4 *5 * Esta é uma alternativa para {approve} que pode ser usada como uma mitigação para6 * os problemas descritos em {IERC20-approve}.7 *8 * Emite um evento {Approval} indicando a permissão atualizada.9 *10 * Requisitos:11 *12 * - `spender` não pode ser o endereço zero.13 * - `spender` deve ter uma permissão para o chamador de pelo menos14 * `subtractedValue`.15 */16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,18 "ERC20: decreased allowance below zero"));19 return true;20 }Exibir tudoFunções que modificam as 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
1 /**2 * @dev Move `amount` de tokens de `sender` para `recipient`.3 *4 * Esta função interna é equivalente a {transfer} e pode ser usada para5 * implementar, por exemplo, taxas automáticas de tokens, mecanismos de slashing, etc.6 *7 * Emite um evento {Transfer}.8 *9 * Requisitos:10 *11 * - `sender` não pode ser o endereço zero.12 * - `recipient` não pode ser o endereço zero.13 * - `sender` deve ter um saldo de pelo menos `amount`.14 */15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {Exibir tudoEsta 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).
1 require(sender != address(0), "ERC20: transfer from the zero address");2 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.
1 _beforeTokenTransfer(sender, recipient, amount);2Existem duas maneiras de usar esse contrato:
- Use-o 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 demonstrou ser seguro. Quando você usa herança, fica claro quais são as funções que você modifica, e para confiar em seu contrato, as pessoas só precisam auditar essas funções específicas.
Muitas vezes, é útil executar uma função cada vez que os tokens trocam 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 essa função e ela será chamada em cada transferência.
1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");2 _balances[recipient] = _balances[recipient].add(amount);Estas são as linhas que realmente fazem a transferência. Note que não há nada entre elas, e que subtraímos o valor transferido do remetente antes de adicioná-lo ao destinatário. Isso é importante porque, se houvesse uma chamada para um contrato diferente no meio, isso poderia ter sido usado para enganar este contrato. Desta forma, a transferência é atômica, nada pode acontecer no meio dela.
1 emit Transfer(sender, recipient, amount);2 }Finalmente, emita um evento Transfer. Eventos não são acessíveis a contratos inteligentes, mas o código executado fora da blockchain
pode ouvir eventos e reagir a eles. Por exemplo, uma carteira pode acompanhar quando o proprietário recebe mais tokens.
As funções _mint e _burn
Essas duas funções (_mint e _burn) modificam o fornecimento 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 criar (mint) novos tokens ou queimar (burn) os existentes.
NOTA: todo token ERC-20 tem sua própria lógica de negócios que dita o gerenciamento de tokens.
Por exemplo, um contrato de fornecimento 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.
1 /** @dev Cria `amount` de tokens e os atribui a `account`, aumentando2 * o fornecimento total.3 *4 * Emite um evento {Transfer} com `from` definido para o endereço zero.5 *6 * Requisitos:7 *8 * - `to` não pode ser o endereço zero.9 */10 function _mint(address account, uint256 amount) internal virtual {11 require(account != address(0), "ERC20: mint to the zero address");12 _beforeTokenTransfer(address(0), account, amount);13 _totalSupply = _totalSupply.add(amount);14 _balances[account] = _balances[account].add(amount);15 emit Transfer(address(0), account, amount);16 }Exibir tudoCertifique-se de atualizar _totalSupply quando o número total de tokens mudar.
1 /**2 * @dev Destrói `amount` de tokens da `account`, reduzindo o3 * fornecimento total.4 *5 * Emite um evento {Transfer} com `to` definido para o endereço zero.6 *7 * Requisitos:8 *9 * - `account` não pode ser o endereço zero.10 * - `account` deve ter pelo menos `amount` de tokens.11 */12 function _burn(address account, uint256 amount) internal virtual {13 require(account != address(0), "ERC20: burn from the zero address");1415 _beforeTokenTransfer(account, address(0), amount);1617 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");18 _totalSupply = _totalSupply.sub(amount);19 emit Transfer(account, address(0), amount);20 }Exibir tudoA 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 não é um problema, porque o saldo é verificado no momento da transferência, quando poderia ser diferente do saldo quando a permissão foi criada.
1 /**2 * @dev Define `amount` como a permissão de `spender` sobre os tokens do `owner`.3 *4 * Essa função interna é equivalente a `approve` e pode ser usada para5 * por exemplo, definir permissões automáticas para certos subsistemas, etc.6 *7 * Emite um evento {Approval}.8 *9 * Requisitos:10 *11 * - `owner` não pode ser o endereço zero.12 * - `spender` não pode ser o endereço zero.13 */14 function _approve(address owner, address spender, uint256 amount) internal virtual {15 require(owner != address(0), "ERC20: approve from the zero address");16 require(spender != address(0), "ERC20: approve to the zero address");1718 _allowances[owner][spender] = amount;Exibir tudo
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.
1 emit Approval(owner, spender, amount);2 }3Modificar a variável Decimals
123 /**4 * @dev Define {decimals} para um valor diferente do padrão de 18.5 *6 * AVISO: Esta função só deve ser chamada a partir do construtor. A maioria dos7 * aplicativos que interagem com contratos de token não esperam que8 * {decimals} mude, e podem funcionar incorretamente se isso acontecer.9 */10 function _setupDecimals(uint8 decimals_) internal {11 _decimals = decimals_;12 }Exibir tudoEsta função modifica a variável _decimals, que é usada para dizer às interfaces de usuário como interpretar o valor.
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
12 /**3 * @dev Gancho (hook) que é chamado antes de qualquer transferência de tokens. Isso inclui4 * criação (minting) e queima (burning).5 *6 * Condições de chamada:7 *8 * - quando `from` e `to` são ambos diferentes de zero, `amount` dos tokens de `from`9 * serão transferidos para `to`.10 * - quando `from` é zero, `amount` de tokens serão criados (minted) para `to`.11 * - quando `to` é zero, `amount` dos tokens de `from` serão queimados (burned).12 * - `from` e `to` nunca são ambos zero.13 *14 * Para saber mais sobre ganchos (hooks), acesse xref:ROOT:extending-contracts.adoc#using-hooks[Usando Ganchos (Hooks)].15 */16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }17}Exibir tudoEsta é a função de gancho (hook) a ser chamada durante as transferências. Está vazia aqui, mas se você precisar que ela faça algo, basta substituí-la.
Conclusão
Para revisar, 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. Esta é 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 verificá-lo (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 local específico, porque isso facilita a auditoria.
Esta é a razão pela qual temos, por exemplo,
_approve, que é chamado porapprove,transferFrom,increaseAllowanceedecreaseAllowance - As mudanças de estado devem ser atômicas, sem qualquer 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 adiciona ao saldo do destinatário, existem menos tokens em existência do que deveria haver. Isso poderia ser potencialmente explorado 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.
Veja aqui mais do meu trabalho (opens in a new tab).
Última atualização da página: 22 de outubro de 2025
