Pular para o conteúdo principal

Demonstração do Contrato ERC-20

solidezerc-20
Iniciante
Ori Pomerantz
9 de março de 2021
27 minutos de leitura minute read

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. Essas moedas seguem a norma ERC-20. Essa norma possibilita a criação de ferramentas, como os pools de liquidez e carteiras, que funcionam com todos os tokens ERC-20. Neste artigo, analisaremos a Implementação do OpenZeppelin Solidity ERC20(opens in a new tab), assim como a definição de interface(opens in a new tab).

Este é o código-fonte anotado. Se você deseja implementar ERC-20, leia este tutorial(opens in a new tab).

A 'Interface'

O objetivo de uma norma como a ERC-20 é permitir que as implementações das várias moedas sejam interoperáveis entre aplicativos, como carteiras e corretoras descentralizadas. Para atingirmos tal objetivo, criamos uma 'interface'(opens in a new tab). Qualquer código que necessite utilizar o contrato pode usar as mesmas definições de interface e ser compatível com todos os contratos de token que o usem, seja uma carteira de criptomoedas como a MetaMask, um aplicativo descentralizado como o Etherscan.io, ou um contrato diferente como um pool de liquidez.

Ilustração da interface ERC-20

Se você é um programador experiente, provavelmente se lembra de ver constructos semelhantes em Java(opens in a new tab) ou mesmo em arquivos de cabeçalho em C(opens in a new tab).

Essa é a definição da interface ERC-20(opens in a new tab) do OpenZeppelin. Ela é uma tradução do padrão legível para humanos(opens in a new tab) em código Solidity. Obviamente, a interface por si só não define como fazer algo. Isso é explicado no código-fonte do contrato abaixo.

1// SPDX-License-Identifier: MIT
Copiar

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ê necessitar de uma licença diferente, explique nos comentários.

1pragma solidity >=0.6.0 <0.8.0;
Copiar

A linguagem Solidity continua evoluindo rapidamente, e novas versões podem não ser compatíveis com o código antigo. (confira 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 com a qual você testou o código.

1/**
2 * @dev Interface of the ERC20 standard as defined in the EIP.
3 */
Copiar

O @dev no comentário faz parte do formato NatSpec(opens in a new tab), usado para produzir a documentação a partir de um código-fonte.

1interface IERC20 {
Copiar

Convenientemente, nomes de Interface começam com I.

1 /**
2 * @dev Returns the amount of tokens in existence.
3 */
4 function totalSupply() external view returns (uint256);
Copiar

Essa função é external, ou seja, só pode ser chamada de fora do contrato(opens in a new tab). Ela retorna o fornecimento total de tokens no contrato. Esse valor é retornado usando o tipo mais comum no Ethereum, 256 bits não assinado (256 bits é o tamanho de fonte nativo da EVM). Essa função também é uma view, ou seja, ela não pode alterar o estado, portanto, ela pode ser executada em apenas um nó em vez de fazer com que todos os nós da blockchain a executem. Esse tipo de função não gera transação e não custa gás.

Observação: Em teoria, pode-se ter a impressão de que o criador do contrato conseguiria trapacear retornando uma quantia menor do fornecimento total do que a quantia real, fazendo com que cada moeda valha mais do que realmente vale. De qualquer forma, este medo ignora a verdadeira natureza da blockchain. Tudo que acontece na blockchain pode ser verificado em cada nó. Para conseguir isso, cada contrato da linguagem de código e armazenamento esta disponível em cada nó. Embora não seja obrigatório publicar o código Solidity, mas ninguém confiará em você a menos que publique o código-fonte e a versão do Solidity usados na compilação, para que ele possa ser comparado com o código de linguagem da máquina que você forneceu. Por exemplo, confira este contrato(opens in a new tab).

1 /**
2 * @dev Returns the amount of tokens owned by `account`.
3 */
4 function balanceOf(address account) external view returns (uint256);
Copiar

Como o próprio nome já diz, balanceOf retorna o saldo de uma conta. Contas de Ethereum são identificadas em Solidity usando address, que contem 160 bits. Também são external e view.

1 /**
2 * @dev Moves `amount` tokens from the caller's account to `recipient`.
3 *
4 * Returns a boolean value indicating whether the operation succeeded.
5 *
6 * Emits a {Transfer} event.
7 */
8 function transfer(address recipient, uint256 amount) external returns (bool);
Copiar

A função transfer transfere as moedas de um chamador para outro endereço. Isso envolve uma mudança de estado, então não é um view. Quando um usuário chama essa função, ele cria uma transação a um custo cobrada em gás. Ele também emite um evento, Transfer, para informar a todos na blockchain sobre esse evento.

Essa função possui duas saídas para dois chamadores diferentes:

  • Os 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 demorar uma quantidade indefinida de tempo. O usuário pode ver o que ocorreu procurando pelo recibo da transação (identificado pela transação hash) ou procurando pelo evento transfer.
  • Outros contratos, nos quais chamam a função como parte de uma transação inteira. Esses contratos obtêm o resultado imediatamente, pois eles executam a mesma transação, para usar o valor de retorno da função.

O mesmo tipo de saída é criado por outras funções que mudam o estado do contrato.

As provisões permitem que uma conta gaste tokens que pertencem a um proprietário diferente. Isso é útil, por exemplo, para contratos que agem como vendedores. Contratos não podem monitorar eventos, portanto, se um comprador quiser transferir diretamente, tokens para o contrato do vendedor, esse contrato não saberá se 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 do contrato do vendedor, para que o contrato do vendedor possa saber se a operação foi bem-sucedida.

1 /**
2 * @dev Returns the remaining number of tokens that `spender` will be
3 * allowed to spend on behalf of `owner` through {transferFrom}. Isso é
4 * zero por padrão.
5 *
6 * This value changes when {approve} or {transferFrom} are called.
7 */
8 function allowance(address owner, address spender) external view returns (uint256);
Copiar

A função allowance permite que qualquer pessoa consulte qual é a provisão que um endereço (owner) permite que outro endereço (spender) gaste.

1 /**
2 * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
3 *
4 * Returns a boolean value indicating whether the operation succeeded.
5 *
6 * IMPORTANT: Beware that changing an allowance with this method brings the risk
7 * that someone may use both the old and the new allowance by unfortunate
8 * transaction ordering. Uma solução possível para mitigar esta corrida
9 * é primeiramente reduzir a tolerância do remetente para 0 e definir o
10 * valor desejado depois:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
13 * Emite um evento de {Approval}.
14 */
15 function approve(address spender, uint256 amount) external returns (bool);
Exibir tudo
Copiar

A função approve cria uma provisã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 é possível controlar a ordem na qual as transações de outras pessoas serão executadas, a menos que você não envie sua própria transação até ver a transação de outro lado ser executada.

1 /**
2 * @dev Moves `amount` tokens from `sender` to `recipient` using the
3 * allowance mechanism. O valor é então deduzido do rendimento do chamador.
4 *
5 * Returns a boolean value indicating whether the operation succeeded.
6 *
7 * Emits a {Transfer} event.
8 */
9 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
Exibir tudo
Copiar

Por fim, transferFrom é utilizado pelo cliente para realmente gastar a provisão.

1
2 /**
3 * @dev Emitted when `value` tokens are moved from one account (`from`) to
4 * another (`to`).
5 *
6 * Note that `value` may be zero.
7 */
8 event Transfer(address indexed from, address indexed to, uint256 value);
9
10 /**
11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by
12 * a call to {approve}. `Valor` é a nova permissão.
13 */
14 event Approval(address indexed owner, address indexed spender, uint256 value);
15}
Exibir tudo
Copiar

Esses eventos são emitidos quando o estado do contrato ERC-20 é alterado.

O contrato real

Este é o contrato que implementa o padrão ERC-20, retirado daqui(opens in a new tab). Ele não é destinado a ser usado tal como é, mas você pode herdar(opens in a new tab) dele para estendê-lo para algo utilizável.

1// SPDX-License-Identifier: MIT
2pragma solidity >=0.6.0 <0.8.0;
Copiar

Importar declarações

Além das definições de interface acima, o contrato de definição importa outros dois arquivos:

1
2import "../../GSN/Context.sol";
3import "./IERC20.sol";
4import "../../math/SafeMath.sol";
Copiar

Este comentário explica o propósito do contrato.

1/**
2 * @dev Implementation of the {IERC20} interface.
3 *
4 * This implementation is agnostic to the way tokens are created. Isto significa
5* que um mecanismo de oferta deve ser adicionado em um contrato derivado usando {_mint}.
6 * For a generic mechanism see {ERC20PresetMinterPauser}.
7 *
8 * TIP: For a detailed writeup see our guide
9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
10 * to implement supply mechanisms].
11 *
12 * We have followed general OpenZeppelin guidelines: functions revert instead
13 * of returning `false` on failure. Esse comportamento é, no entanto, convencional
14 * e não entra em conflito com as expectativas das aplicações do ERC20.
15 *
16 * Additionally, an {Approval} event is emitted on calls to {transferFrom}.
17 * This allows applications to reconstruct the allowance for all accounts just
18 * by listening to said events. Outras implementações do EIP podem não emitir
19 * esses eventos, pois não é exigido pela especificação.
20 *
21 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
22 * functions have been added to mitigate the well-known issues around setting
23 * allowances. Veja {IERC20-approve}.
24 */
25
Exibir tudo
Copiar

Definição de contrato

1contract ERC20 is Context, IERC20 {
Copiar

Esta linha especifica a herança, neste caso de IERC20 acima e Context, para OpenGSN.

1
2 using SafeMath for uint256;
3
Copiar

Essa linha anexa a biblioteca SafeMath ao tipo uint256. Você pode encontrar essa biblioteca aqui(opens in a new tab).

Definições de variáveis

Essas definições especificam as variáveis de estado do contrato. Existem variáveis declaradas como private, mas isso apenas significa que outros contratos na blockchain não as podem ler. Não há segredos na blockchain, o software em cada nó possui o estado de cada contrato em cada bloco. Por convenção, as variáveis de estado são denominadas _<something>.

As duas primeiras variáveis são mapeamentos(opens in a new tab), ou seja, se comportam mais ou menos da mesma forma que matrizes associativas(opens in a new tab), com exceção das chaves, que são valores numéricos. O armazenamento é alocado apenas para as entradas que possuem valores diferentes do padrão (zero).

1 mapping (address => uint256) private _balances;
Copiar

O primeiro mapeamento, _balances, é composta por endereços e seus respectivos saldos desse token. Para acessar o saldo, utilize a sintaxe: _balances[<address>].

1 mapping (address => mapping (address => uint256)) private _allowances;
Copiar

Esta variável, _allowances, armazena as margens explicadas anteriormente. O primeiro índice é o proprietário das moedas, e o segundo é o contrato com a provisão. Para acessar a quantia que o endereço A pode gastar na conta do endereço B, use _allowances[B][A].

1 uint256 private _totalSupply;
Copiar

Como o nome sugere, essa variável mantém registro do fornecimento total de tokens.

1 string private _name;
2 string private _symbol;
3 uint8 private _decimals;
Copiar

Essas três variáveis são usadas para melhorar a legibilidade. As duas primeiras são autoexplicativas, mas _decimals não.

De um lado, o Ethereum não possui ponto flutuante ou variáveis fracionadas. De outro, as pessoas gostam de poder dividir tokens. Uma das razões pelas quais as pessoas estabeleceram o uso do ouro como moeda foi devido à dificuldade de trocá-lo quando alguém queria, por exemplo, comprar vaca pelo valor de um pato.

A solução é manter o registro dos inteiros, mas em vez de contar o token real, contar o token fracionário, que praticamente não tem valor. No caso do ether, a moeda fracionária é chamada de wei, e 10^18 WEI é igual a um ETH. No momento da criação deste artigo, 10.000.000.000.000 WEI equivalem a cerca de um centavo de Dólar 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, seria equivalente a 3,14 ETH? 31,41 ETH? 3,141 ETH? No caso do ETH, é definido 10^18 WEI para o ETH, mas para sua moeda, você pode escolher um valor diferente. Se dividir uma moeda não fizer sentido, você pode usar um valor _decimals de zero. Se você quiser utilizar o mesmo padrão utilizado em ETH, use o valor 18.

O Constructor

1 /**
2 * @dev Sets the values for {name} and {symbol}, initializes {decimals} with
3 * a default value of 18.
4 *
5 * To select a different value for {decimals}, use {_setupDecimals}.
6 *
7 * All three of these values are immutable: they can only be set once during
8 * construction.
9 */
10 constructor (string memory name_, string memory symbol_) public {
11 _name = name_;
12 _symbol = symbol_;
13 _decimals = 18;
14 }
Exibir tudo
Copiar

O construtor é chamado quando o contrato é criado pela primeira vez. Por convenção, os parâmetros da função são denominados <something>_.

Funções da interface do usuário

1 /**
2 * @dev Returns the name of the token.
3 */
4 function name() public view returns (string memory) {
5 return _name;
6 }
7
8 /**
9 * @dev Returns the symbol of the token, usually a shorter version of the
10 * name.
11 */
12 function symbol() public view returns (string memory) {
13 return _symbol;
14 }
15
16 /**
17 * @dev Returns the number of decimals used to get its user representation.
18 * For example, if `decimals` equals `2`, a balance of `505` tokens should
19 * be displayed to a user as `5,05` (`505 / 10 ** 2`).
20 *
21 * Tokens usually opt for a value of 18, imitating the relationship between
22 * ether and wei. Esse é o valor {ERC20} usado, a menos que {_setupDecimals} seja
23 * chamado.
24 *
25 * NOTE: This information is only used for _display_ purposes: it in
26 * no way affects any of the arithmetic of the contract, including
27 * {IERC20-balanceOf} and {IERC20-transfer}.
28 */
29 function decimals() public view returns (uint8) {
30 return _decimals;
31 }
Exibir tudo
Copiar

Essas funções, nome, symbol e decimals, ajudam as interfaces do usuário a conhecer o seu contrato para poderem exibi-lo corretamente.

O tipo do retorno é string memory, o que significa que retorna uma string que é armazenada na memória. Variáveis, como ‘strings’, podem ser armazenadas em três locais:

Tempo de vidaAcesso ao contratoCusto em gás
MemóriaChamada da funçãoLeitura/gravaçãoDezenas ou centenas (maior para locais mais altos)
CalldataChamar FunçãoSomente leituraNão pode ser usada como retorno, apenas como tipo de parâmetro de função
ArmazenamentoAté ser alteradoLer/EscreverAlto (800 para leitura, 20 mil para gravação)

Neste caso, memory é a melhor escolha.

Informação de leitura do token

Essas funções fornecem informações sobre o token, seja o fornecimento total ou o saldo de uma conta.

1 /**
2 * @dev See {IERC20-totalSupply}.
3 */
4 function totalSupply() public view override returns (uint256) {
5 return _totalSupply;
6 }
Copiar

A função totalSupply retorna o fornecimento total de tokens.

1 /**
2 * @dev See {IERC20-balanceOf}.
3 */
4 function balanceOf(address account) public view override returns (uint256) {
5 return _balances[account];
6 }
Copiar

Leia o saldo de uma conta. Observe que qualquer um pode obter o saldo da conta de outra pessoa. Não há motivo para esconder essa informação, pois ela está disponível em todos os nós. Não há segredos na blockchain.

Transferência de tokens

1 /**
2 * @dev See {IERC20-transfer}.
3 *
4 * Requirements:
5 *
6 * - `recipient` cannot be the zero address.
7 * - the caller must have a balance of at least `amount`.
8 */
9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
Exibir tudo
Copiar

A função transfer é chamada para transferir os tokens do remetente para um destinatário. Observe que mesmo que ela retorne um valor booleano, o valor é sempre true. Se a transferência falhar, o contrato anulará a chamada.

1 _transfer(_msgSender(), recipient, amount);
2 return true;
3 }
Copiar

A função _transfer faz o trabalho real. Ela é uma função privada que só pode ser chamada por outras funções de contrato. Por convenção, funções privadas são denominadas _<something>, assim como as variáveis de estado.

Normalmente, usamos msg.sender no Solidity para o remetente de mensagens. No entanto, isso rompe a OpenGSN(opens in a new tab). Caso queiramos permitir transações sem Eth com nosso token, precisamos usar _msgSender(). Ela retornará msg.sender para transações normais, mas para transações sem Eth, ela indicará o signatário original e não o contrato que repassou a mensagem.

Funções de margem

Estas são as funções que implementam a funcionalidade da margem: allowance, approve, transferFrom, e _approve. Além disso, a implementação do OpenZeppelin vai além do padrão básico, para poder incluir alguns recursos que melhoram a segurança: increaseAllowance, e decreaseAllowance.

A função allowance

1 /**
2 * @dev See {IERC20-allowance}.
3 */
4 function allowance(address owner, address spender) public view virtual override returns (uint256) {
5 return _allowances[owner][spender];
6 }
Copiar

A função allowance permite que todo mundo confira qualquer margem.

A função approve

1 /**
2 * @dev See {IERC20-approve}.
3 *
4 * Requirements:
5 *
6 * - `spender` cannot be the zero address.
7 */
8 function approve(address spender, uint256 amount) public virtual override returns (bool) {
Copiar

Essa função é chamada para criar uma provisão. Ela é semelhante à função transfer acima:

  • A função apenas chama uma função interna (neste caso, _approve) que realmente faz o trabalho.
  • A função retorna true (se for bem-sucedida) ou é revertida (se falhar).
1 _approve(_msgSender(), spender, amount);
2 return tu;
3}
Copiar

Usamos funções internas para minimizar a quantidade de lugares onde as mudanças de estado ocorrem. Qualquer função que mude o estado constitui um risco de segurança em potencial que precisa ser auditado para segurança. Dessa forma, temos menos chances de errar.

A função transferFrom

Essa é a função que um gastador chama para gastar uma margem. Isso requer duas operações: transfira o valor sendo gasto e reduza a margem nesse valor.

1 /**
2 * @dev See {IERC20-transferFrom}.
3 *
4 * Emits an {Approval} event indicating the updated allowance. Isso não é
5 * necessário para o EIP. Veja a nota no início do {ERC20}.
6 *
7 * Requirements:
8 *
9 * - `sender` and `recipient` cannot be the zero address.
10 * - `sender` must have a balance of at least `amount`.
11 * - the caller must have allowance for ``sender``'s tokens of at least
12 * `amount`.
13 */
14 function transferFrom(address sender, address recipient, uint256 amount) public virtual
15 override returns (bool) {
16 _transfer(sender, recipient, amount);
Exibir tudo
Copiar

A chamada da função a.sub(b, "message") faz duas coisas. Primeiro, ela calcula a-b, que é a nova margem. Em seguida, ela verifica se esse resultado não é negativo. Se for negativo, a chamada é revertida com a mensagem fornecida. Observe que, quando uma chamada reverte qualquer processamento feito anteriormente a essa chamada, ela é ignorada para não precisarmos desfazer a _transfer.

1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
2 "ERC20: transfer amount exceeds allowance"));
3 return true;
4 }
Copiar

Adições de segurança do OpenZeppelin

É perigoso definir uma margem que não seja zero como outro valor que não seja zero, porque você só controla a ordem de suas próprias transações, mas não as de outras pessoas. Imagine que você tenha dois usuários: Alice, que é ingênua, e Bill, que é desonesto. Alice quer solicitar um serviço de Bill que, segundo ela, custa cinco tokens — então, ela dá a Bill uma provisão de cinco tokens.

Então, algo muda e o preço de Bill aumenta para dez tokens. Alice, que ainda quer o serviço, envia uma transação que define a provisã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 com uma tarifa de gás muito mais alta que, portanto, será minerada mais rápido. Dessa forma, Bill pode gastar os cinco primeiros tokens e, quando a nova provisão de Alice for minerada, pode gastar mais dez por um preço total de quinze tokens, mais do que Alice queria autorizar. Essa técnica é chamada de front-running(opens in a new tab)

Transação de AliceNonce de AliceTransação de BillNonce de BillA provisão de BillTotal faturado por Bill de Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10.12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10.124015

Para evitar esse problema, essas duas funções (increaseAllowance e reduaseAllowance) permitem que você modifique a provisão por um valor específico. Então, se Bill já tinha gastado cinco tokens, ele só poderá gastar mais cinco tokens. Dependendo do tempo disponível, há duas maneiras de proceder, sendo que as duas acabam com Bill obtendo os dez tokens:

A:

Transação de AliceNonce de AliceTransação de BillNonce de BillPermissão de BillCobrança Total de Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Transação de AliceNonce de AliceTransação de BillNonce de BillPermissão de BillCobrança Total de Alice
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010
1 /**
2 * @dev Atomically increases the allowance granted to `spender` by the caller.
3 *
4 * This is an alternative to {approve} that can be used as a mitigation for
5 * problems described in {IERC20-approve}.
6 *
7 * Emits an {Approval} event indicating the updated allowance.
8 *
9 * Requirements:
10 *
11 * - `spender` cannot be the zero address.
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 tudo
Copiar

A função a.add(b) é uma adição segura. No caso improvável de um+b>=2^256, ele não é contornado da mesma maneira que uma adição normal.

1
2 /**
3 * @dev Atomically decreases the allowance granted to `spender` by the caller.
4 *
5 * This is an alternative to {approve} that can be used as a mitigation for
6 * problems described in {IERC20-approve}.
7 *
8 * Emits an {Approval} event indicating the updated allowance.
9 *
10 * Requirements:
11 *
12 * - `spender` cannot be the zero address.
13 * - `spender` must have allowance for the caller of at least
14 * `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 tudo
Copiar

Funções que modificam informações do token

Essas são as quatro funções que realmente funcionam: _transfer, _mint, _burn, e _appro.

A função _transfer {#_transfer}

1 /**
2 * @dev Moves tokens `amount` from `sender` to `recipient`.
3 *
4 * This is internal function is equivalent to {transfer}, and can be used to
5 * e.g. implement automatic token fees, slashing mechanisms, etc.
6 *
7 * Emits a {Transfer} event.
8 *
9 * Requirements:
10 *
11 * - `sender` cannot be the zero address.
12 * - `recipient` cannot be the zero address.
13 * - `sender` must have a balance of at least `amount`.
14 */
15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {
Exibir tudo
Copiar

Esta função, _transfer, transfere moedas de uma conta para outra. Ela é chamada por transfer (para transferências da própria conta do remetente) e transferFrom (para usar as provisões a serem transferidas 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");
Copiar

Na verdade, ninguém possui o endereço zero no Ethereum (ou seja, ninguém conhece uma chave privada cuja chave pública correspondente tenha sido transformada no endereço zero). Quando as pessoas usam esse endereço, geralmente se trata de um bug de software, portanto, falhamos se o endereço zero é usado como o remetente ou o destinatário.

1 _beforeTokenTransfer(sender, recipient, amount);
2
Copiar

Existem duas maneiras de usar esse contrato:

  1. Use-o como um modelo para o seu próprio código
  2. Herde a partir daqui(opens in a new tab) e substitua apenas as funções que você precisa modificar

O segundo método é muito melhor, porque o código OpenZeppelin ERC-20 já foi auditado e comprovado como seguro. Ao usar a herança, é fácil distinguir quais são as funções que você modificou e, para confiar nos seus contratos, as pessoas só precisam auditar essas funções específicas.

Geralmente, é útil executar uma função toda vez que os tokens mudam de mãos. No entanto,_transfer é uma função muito importante e é possível escrevê-la de forma não segura (veja abaixo). Portanto, é melhor não substituí-la. A solução é _beforeTokenTransfer, uma função 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);
Copiar

Essas são as linhas que realmente executam a transferência. Observe 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, pois se tivesse ocorrido uma chamada para um contrato diferente nesse meio tempo, ela poderia ter sido utilizada para enganar esse contrato. Dessa forma, a transferência é atômica, ou seja, nada pode acontecer enquanto ela está em execução.

1 emit Transfer(sender, recipient, amount);
2 }
Copiar

Essa função emite o evento Transfer. Os eventos não são acessíveis para contratos inteligentes, mas o código executado fora da blockchain pode ouvir os eventos e reagir a eles. Por exemplo, uma carteira pode monitorar quando o proprietário obtém mais tokens.

As funções _mint e _burn {#_mint-and-_burn}

Essas duas funções (_mint e _burn) modificam o fornecimento total de moedas. Elas são internas e não há nenhuma função que as chame nesse contrato, portanto, elas só são úteis se você herdar do contrato e adicionar sua própria lógica para decidir em que condições gerar novos tokens ou usar os tokens já existentes.

OBSERVAÇÃO: Todos os tokens ERC-20 têm sua própria lógica comercial que dita o gerenciamento de tokens. Por exemplo, um contrato de fornecimento fixo só pode chamar _mint no construtor e nunca chamar _burn. Um contrato que vende tokens chamará _mint quando for pago, e provavelmente chamará _burn em algum momento para evitar hiperinflação.

1 /** @dev Creates `amount` tokens and assigns them to `account`, increasing
2 * the total supply.
3 *
4 * Emits a {Transfer} event with `from` set to the zero address.
5 *
6 * Requirements:
7 *
8 * - `to` cannot be the zero address.
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 tudo
Copiar

Certifique-se de atualizar o _totalSupply quando o número total de tokens mudar.

1 /**
2 * @dev Destroys `amount` tokens from `account`, reducing the
3 * total supply.
4 *
5 * Emits a {Transfer} event with `to` set to the zero address.
6 *
7 * Requirements:
8 *
9 * - `account` cannot be the zero address.
10 * - `account` must have at least `amount` tokens.
11 */
12 function _burn(address account, uint256 amount) internal virtual {
13 require(account != address(0), "ERC20: burn from the zero address");
14
15 _beforeTokenTransfer(account, address(0), amount);
16
17 _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 tudo

A função _burn é quase idêntica à _mint, exceto que ela funciona na direção inversa.

A função _approve {#_approve}

Essa é a função que especifica as provisões. Observe que ela permite que um proprietário especifique uma provisão superior ao saldo atual do proprietário. Isso não tem problema, pois o saldo é verificado no momento da transferência, quando ele poderia diferir do saldo no momento da criação da provisão.

1 /**
2 * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens.
3 *
4 * This internal function is equivalent to `approve`, and can be used to
5 * e.g. set automatic allowances for certain subsystems, etc.
6 *
7 * Emits an {Approval} event.
8 *
9 * Requirements:
10 *
11 * - `owner` cannot be the zero address.
12 * - `spender` cannot be the zero address.
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");
17
18 _allowances[owner][spender] = amount;
Exibir tudo
Copiar

Emita um evento Approval. Dependendo de como o aplicativo é escrito, o contrato do gastador pode ser informado sobre a aprovação, seja pelo proprietário, seja pelo servidor que realiza esses eventos.

1 emit Approval(owner, spender, amount);
2 }
3
Copiar

Modificando as variáveis decimais

1
2
3 /**
4 * @dev Sets {decimals} to a value other than the default one of 18.
5 *
6 * WARNING: This function should only be called from the constructor. A maioria dos
7 * aplicativos que interagem com contratos das moedas não esperarão que
8 * {decimals} altere, e pode funcionar incorretamente se ele o fizer.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
Exibir tudo
Copiar

Essa função modifica a variável _decimals utilizada 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, ainda mais que aplicativos não são projetados para lidar com isso.

Ganchos

1
2 /**
3 * @dev Hook that is called before any transfer of tokens. This includes
4 * minting and burning.
5 *
6 * Calling conditions:
7 *
8 * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens
9 * will be to transferred to `to`.
10 * - when `from` is zero, `amount` tokens will be minted for `to`.
11 * - when `to` is zero, `amount` of ``from``'s tokens will be burned.
12 * - `from` and `to` are never both zero.
13 *
14 * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
15 */
16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
17}
Exibir tudo
Copiar

Essa é a função hook a ser chamada durante as transferências. Ela está vazia, mas se precisar dela para fazer algo, basta sobrescrevê-la.

Conclusão

Resumindo, aqui estão algumas das ideias mais importantes neste contrato (na minha opinião, pode ser que as suas não sejam as mesmas):

  • Não há segredos na blockchain. Qualquer informação que um contrato inteligente possa acessar está disponível para o mundo inteiro.
  • Você pode controlar a ordem de suas transações, mas não quando transações de outras pessoas estão em andamento. É por isso que alterar uma provisão pode ser perigoso, porque permite que o gastador gaste a soma das duas provisões.
  • Valores do tipo uint256 aproximados. Em outras palavras, 0-1=2^256-1. Se esse não for o comportamento desejado, você precisa verificá-lo (ou usar a biblioteca SafeMath que faz isso por você). Observe que isso foi alterado em Solidity 0.8.0(opens in a new tab).
  • Faça todas as mudanças de estado de um tipo específico e em um local específico, pois isso facilita a auditoria. Esse é o motivo pelo qual temos, por exemplo, _approve, chamado por approve, transferFrom, increaseAllowance e decreaseAllowance
  • Mudanças de estado devem ser atômicas, sem qualquer outra ação no meio (como se pode ver em _transfer). Isso ocorre, pois, durante a mudança de estado, o estado é inconsistente. Por exemplo, entre o tempo que você deduz do saldo do remetente e o tempo de adicionar ao saldo do beneficiário, há menos tokens existentes do que deveria haver. Isto pode ser potencialmente explorado mal-intencionadamente se houver operações entre eles, especialmente chamadas para um contrato diferente.

Agora que você já viu como o contrato do OpenZeppelin ERC-20 é escrito, e especialmente como ele se tornou mais seguro, escreva seus próprios contratos e aplicativos seguros.

Última edição: @rafarocha(opens in a new tab), 22 de janeiro de 2024

Este tutorial foi útil?