Demonstração 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. 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.
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: MITCopiar
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 be3 * 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 risk7 * that someone may use both the old and the new allowance by unfortunate8 * transaction ordering. Uma solução possível para mitigar esta corrida9 * é primeiramente reduzir a tolerância do remetente para 0 e definir o10 * valor desejado depois:11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472912 *13 * Emite um evento de {Approval}.14 */15 function approve(address spender, uint256 amount) external returns (bool);Exibir tudoCopiar
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 the3 * 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 tudoCopiar
Por fim, transferFrom
é utilizado pelo cliente para realmente gastar a provisão.
12 /**3 * @dev Emitted when `value` tokens are moved from one account (`from`) to4 * another (`to`).5 *6 * Note that `value` may be zero.7 */8 event Transfer(address indexed from, address indexed to, uint256 value);910 /**11 * @dev Emitted when the allowance of a `spender` for an `owner` is set by12 * a call to {approve}. `Valor` é a nova permissão.13 */14 event Approval(address indexed owner, address indexed spender, uint256 value);15}Exibir tudoCopiar
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: MIT2pragma 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:
12import "../../GSN/Context.sol";3import "./IERC20.sol";4import "../../math/SafeMath.sol";Copiar
GSN/Context.sol
são as definições necessárias para usar OpenGSN(opens in a new tab), um sistema que permite que usuários sem ether possam usar a blockchain. Observe que esta é uma versão antiga. Se você quiser integrá-la com o OpenGSN use este tutorial(opens in a new tab).- A biblioteca SafeMath(opens in a new tab), que é usada para fazer adições e subtrações sem estouros. Isso é necessário, pois, do contrário, uma pessoa pode ter um token, dois tokens, e então ter 2^256-1 tokens.
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 significa5* 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 guide9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How10 * to implement supply mechanisms].11 *12 * We have followed general OpenZeppelin guidelines: functions revert instead13 * of returning `false` on failure. Esse comportamento é, no entanto, convencional14 * 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 just18 * by listening to said events. Outras implementações do EIP podem não emitir19 * 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 setting23 * allowances. Veja {IERC20-approve}.24 */25Exibir tudoCopiar
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.
12 using SafeMath for uint256;3Copiar
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} with3 * 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 during8 * construction.9 */10 constructor (string memory name_, string memory symbol_) public {11 _name = name_;12 _symbol = symbol_;13 _decimals = 18;14 }Exibir tudoCopiar
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 }78 /**9 * @dev Returns the symbol of the token, usually a shorter version of the10 * name.11 */12 function symbol() public view returns (string memory) {13 return _symbol;14 }1516 /**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 should19 * be displayed to a user as `5,05` (`505 / 10 ** 2`).20 *21 * Tokens usually opt for a value of 18, imitating the relationship between22 * ether and wei. Esse é o valor {ERC20} usado, a menos que {_setupDecimals} seja23 * chamado.24 *25 * NOTE: This information is only used for _display_ purposes: it in26 * no way affects any of the arithmetic of the contract, including27 * {IERC20-balanceOf} and {IERC20-transfer}.28 */29 function decimals() public view returns (uint8) {30 return _decimals;31 }Exibir tudoCopiar
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 vida | Acesso ao contrato | Custo em gás | |
---|---|---|---|
Memória | Chamada da função | Leitura/gravação | Dezenas ou centenas (maior para locais mais altos) |
Calldata | Chamar Função | Somente leitura | Não pode ser usada como retorno, apenas como tipo de parâmetro de função |
Armazenamento | Até ser alterado | Ler/Escrever | Alto (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 tudoCopiar
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 least12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 override returns (bool) {16 _transfer(sender, recipient, amount);Exibir tudoCopiar
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 Alice | Nonce de Alice | Transação de Bill | Nonce de Bill | A provisão de Bill | Total faturado por 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 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 Alice | Nonce de Alice | Transação de Bill | Nonce de Bill | Permissão de Bill | Cobrança Total 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 | Cobrança Total 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 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 for5 * 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 tudoCopiar
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.
12 /**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 for6 * 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 least14 * `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 tudoCopiar
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 to5 * 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 tudoCopiar
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);2Copiar
Existem duas maneiras de usar esse contrato:
- Use-o como um modelo para o seu próprio código
- 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`, increasing2 * 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 tudoCopiar
Certifique-se de atualizar o _totalSupply
quando o número total de tokens mudar.
1 /**2 * @dev Destroys `amount` tokens from `account`, reducing the3 * 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");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 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 to5 * 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");1718 _allowances[owner][spender] = amount;Exibir tudoCopiar
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 }3Copiar
Modificando as variáveis decimais
123 /**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 dos7 * aplicativos que interagem com contratos das moedas não esperarão que8 * {decimals} altere, e pode funcionar incorretamente se ele o fizer.9 */10 function _setupDecimals(uint8 decimals_) internal {11 _decimals = decimals_;12 }Exibir tudoCopiar
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
12 /**3 * @dev Hook that is called before any transfer of tokens. This includes4 * minting and burning.5 *6 * Calling conditions:7 *8 * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens9 * 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 tudoCopiar
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 porapprove
,transferFrom
,increaseAllowance
edecreaseAllowance
- 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: @nhsz(opens in a new tab), 18 de fevereiro de 2024