Vai al contenuto principale

Guida dettagliata al contratto ERC-20

Solidityerc-20
Principiante
Ori Pomerantz
9 marzo 2021
27 minuti letti minute read

Introduzione

Uno degli utilizzi più comuni di Ethereum è quello di permettere a un gruppo di persone di creare un token scambiabile, che potremmo definire la loro valuta. In genere questi token seguono uno standard, l'ERC-20. Questo standard permette di scrivere gli strumenti, come pool di liquidità e wallet, compatibili con tutti i token ERC-20. In questo articolo analizzeremo l'Implementazione di ERC20 in Solidity su OpenZeppelin(opens in a new tab), nonché la definizione dell'interfaccia(opens in a new tab).

Qui parliamo del codice sorgente annotato. Se vuoi implementare ERC-20, leggi questo tutorial(opens in a new tab).

L'interfaccia

Lo scopo di uno standard come ERC-20 è quello di consentire molte implementazioni di token che siano interoperabili tra le varie applicazioni, quali wallet e scambi decentralizzati. A tale scopo, creiamo un'interfaccia(opens in a new tab). Ogni codice che necessita di usare il contratto del token può avvalersi delle stesse definizioni nell'interfaccia ed essere compatibile con tutti i contratti del token che la usano, che si tratti di un portafoglio come MetaMask, una dApp come etherscan.io o un contratto diverso, come un pool di liquidità.

Illustrazione dell'interfaccia di ERC-20

Se sei un programmatore esperto, probabilmente ricorderai di aver visto costrutti simili in Java(opens in a new tab) o persino nei file d'intestazione in C(opens in a new tab).

Questa è una definizione dell'Interfaccia di ERC-20(opens in a new tab) da OpenZeppelin. È una traduzione dello standard leggibile umano(opens in a new tab) nel codice di Solidity. Ovviamente, l'interfaccia di per sé non definisce come fare qualcosa. Ciò è spiegato nel codice sorgente del contratto di seguito.

1// SPDX-License-Identifier: MIT
Copia

I file di Solidity dovrebbero includere un'identificativo della licenza. Puoi vedere qui l'elenco di licenze(opens in a new tab). Se hai bisogno di una licenza diversa, basta spiegarlo nei commenti.

1pragma solidity >=0.6.0 <0.8.0;
Copia

Il linguaggio Solidity si sta ancora evolvendo rapidamente e le nuove versioni potrebbero non essere compatibili con il vecchio codice (vedi qui(opens in a new tab)). Dunque, è una buona idea specificare non solo una versione minima del linguaggio, ma anche una versione massima, l'ultima con cui hai testato il codice.

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

Il @dev nel commento è parte del formato NatSpec(opens in a new tab), usato per produrre la documentazione dal codice sorgente.

1interface IERC20 {
Copia

Per convenzione, i nomi dell'interfaccia iniziano per I.

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

Questa funzione è external, a significare che può essere chiamata solo dal di fuori del contratto(opens in a new tab). Restituisce la fornitura totale di token nel contratto. Questo valore è restituito usando il tipo più comune in Ethereum, ovvero 256 bit non firmati (256 bit è la dimensione nativa della parola dell'EVM). Questa funzione è anche una view, il che significa che non cambia stato, quindi è eseguibile su un nodo singolo invece di farla eseguire da ciascun nodo nella blockchain. Questo tipo di funzione non genera una transazione e non costa gas.

Nota: In teoria, si potrebbe pensare che il creatore del contratto possa imbrogliare restituendo una fornitura totale inferiore al valore reale, facendo apparire ogni token come più prezioso di quanto sia realmente. Tuttavia, tale timore ignora la vera natura della blockchain. Tutto ciò che succede sulla blockchain è verificabile da ogni nodo. A tale scopo, il codice del linguaggio della macchina e la memoria di ciascun contratto sono disponibili su tutti i nodi. Benché non sia obbligatorio pubblicare il codice di Solidity per il tuo contratto, nessuno ti prenderebbe sul serio se non pubblicassi il codice sorgente e la versione di Solidity con cui lo hai compilato, così da renderlo verificabile rispetto al codice del linguaggio della macchina che hai indicato. Vediamo ad esempio questo contratto(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);
Copia

Come dice il nome balanceOf restituisce il saldo di un conto. I conti di Ethereum sono identificati in Solidity usando il tipo address, contenente 160 bit. È anche 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 * Emette un evento {Transfer}.
7 */
8 function transfer(address recipient, uint256 amount) external returns (bool);
Copia

La funzione transfer trasferisce i token dal chiamante a un indirizzo diverso. Ciò implica un cambio di stato, quindi non è una view. Quando un utente chiama questa funzione, crea una transazione e consuma del gas. Inoltre viene emesso un evento, Transfer, per informare tutti sulla blockchain dell'evento.

La funzione ha due tipi di output per due diversi tipi di chiamanti:

  • Gli utenti che chiamano la funzione direttamente da un'interfaccia utente. In genere l'utente invia una transazione e non attende una risposta, il che potrebbe richiedere un tempo indefinito. L'utente può vedere cosa è successo cercando la ricevuta della transazione (identificata dall'hash della transazione) o cercando l'evento Transfer.
  • Altri contratti, che chiamano la funzione nell'ambito di una transazione complessiva. Questi ottengono il risultato immediatamente, perché sono eseguiti nella transazione stessa, così da poter usare il valore di ritorno della funzione.

Lo stesso tipo di output è creato da altre funzioni che cambiano lo stato del contratto.

Le indennità permettono a un conto di spendere dei token appartenenti a un altro proprietario. Ciò è utile, ad esempio, per i contratti che fungono da venditori. I contratti non possono monitorare gli eventi, quindi se un acquirente dovesse trasferire token al contratto del venditore direttamente, quel contratto non saprebbe di aver ricevuto il pagamento. Invece, l'acquirente permette al contratto del venditore di spendere un certo importo e il venditore trasferisce quell'importo. Questo avviene tramite un funzione chiamata dal contratto del venditore, in modo tale che il contratto del venditore possa sapere se è andata a buon fine.

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

La funzione allowance consente a chiunque di richiedere di vedere quale sia il margine di tolleranza che un indirizzo (owner) permette all'altro indirizzo (spender) di spendere.

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. One possible solution to mitigate this race
9 * condition is to first reduce the spender's allowance to 0 and set the
10 * desired value afterwards:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
13 * Emits an {Approval} event.
14 */
15 function approve(address spender, uint256 amount) external returns (bool);
Mostra tutto
Copia

La funzione approve crea una tolleranza. Assicurati di leggere il messaggio sui rischi di utilizzo improprio. In Ethereum puoi controllare l'ordine delle tue transazioni, ma non puoi controllare l'ordine con cui le transazioni altrui saranno eseguite, a meno che tu tenga in sospeso la tua transazione finché non vedi che la transazione dell'altro lato ha avuto luogo.

1 /**
2 * @dev Moves `amount` tokens from `sender` to `recipient` using the
3 * allowance mechanism. `amount` is then deducted from the caller's
4 * allowance.
5 *
6 * Returns a boolean value indicating whether the operation succeeded.
7 *
8 * Emits a {Transfer} event.
9 */
10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
Mostra tutto
Copia

Infine, transferFrom è usata dallo spender per spendere concretamente il margine di tolleranza.

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}. `value` is the new allowance.
13 */
14 event Approval(address indexed owner, address indexed spender, uint256 value);
15}
Mostra tutto
Copia

Questi eventi sono emessi quando lo stato del contratto ERC-20 cambia.

Il Contratto effettivo

Si tratta del contratto vero e proprio che implementa lo standard ERC-20, preso da qui(opens in a new tab). Non è destinato a essere utilizzato così com'è, ma puoi ereditare(opens in a new tab) la sua struttura ed estenderla per ottenere un qualcosa di utilizzabile.

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

Dichiarazioni relative all'importazione

Oltre alle definizioni d'interfaccia summenzionate, la definizione del contratto importa altri due file:

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

Questo commento spiega lo scopo del contratto.

1/**
2 * @dev Implementation of the {IERC20} interface.
3 *
4 * This implementation is agnostic to the way tokens are created. This means
5 * that a supply mechanism has to be added in a derived contract using {_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. This behavior is nonetheless conventional
14 * and does not conflict with the expectations of ERC20 applications.
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. Other implementations of the EIP may not emit
19 * these events, as it isn't required by the specification.
20 *
21 * Finally, the non-standard {decreaseAllowance} and {increaseAllowance}
22 * functions have been added to mitigate the well-known issues around setting
23 * allowances. See {IERC20-approve}.
24 */
25
Mostra tutto
Copia

Composizione del contratto

1contract ERC20 is Context, IERC20 {
Copia

Questa riga specifica l'eredità, in questo caso da IERC20 da sopra e Context, per OpenGSN.

1
2 using SafeMath for uint256;
3
Copia

Questa riga allega la libreria SafeMath al tipo uint256. Puoi trovare questa libreria qui(opens in a new tab).

Definizioni delle variabili

Queste definizioni specificano le variabili di stato del contratto. Queste variabili sono dichiarate come private, ma ciò significa solo che gli altri contratti sulla blockchain non possono leggerle. Non ci sono segreti sulla blockchain, il software su ogni nodo ha lo stato di ciascun contratto in ogni blocco. Per convenzione, le variabili di stato sono denominate _<something>.

Le prime due variabili sono di mappatura(opens in a new tab), il che significa che si comportano più o meno come insiemi associativi(opens in a new tab), se con la differenza che le chiavi sono valori numerici. La memoria è allocata solo per le voci che hanno valori differenti dal default (zero).

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

La prima mappatura, _balances è composta da indirizzi e dai rispettivi saldi di questo token. Per accedere al saldo, usa questa sintassi: _balances[<address>].

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

Questa variabile, _allowances, memorizza i margini di tolleranza spiegati in precedenza. Il primo indice è il proprietario dei token e il secondo è il contratto con il margine di tolleranza. Per accedere all'importo che l'indirizzo A può spendere dal conto dell'indirizzo B, usa _allowances[B][A].

1 uint256 private _totalSupply;
Copia

Come suggerisce il nome, questa variabile tiene traccia della fornitura totale di token.

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

Queste tre variabili sono usate per migliorare la leggibilità. Le prime due sono autoesplicative, ma _decimals no.

Da un lato, Ethereum non ha un numero in virgola mobile o variabili frazionali. Dall'altro, gli esseri vogliono la libertà di dividere i token. Un motivo per cui è stato scelto l'oro per gli scambi era la difficoltà di dare il resto quando qualcuno voleva comprare una quantità di mucca equivalente a un'anatra.

La soluzione è tenere traccia degli interi e, al posto del token reale, contare un token frazionale, quasi privo di valore. Nel caso dell'ether, il token frazionale è detto wei e 10^18 wei sono pari a un ETH. Mentre scriviamo questo articolo, 10.000.000.000.000 wei corrispondono a circa un centesimo di dollaro americano o di euro.

Le applicazioni devono sapere come mostrare il saldo di token. Se un utente ha 3.141.000.000.000.000.000 wei, vuol dire che ha in mano 3,14 ETH? O forse 31,41 ETH? O magari 3.141 ETH? Nel caso dell'ether, è definito a 10^18 wei per ETH, ma per il tuo token, puoi selezionare un valore differente. Se dividere il token non ha senso, puoi usare un valore _decimals di zero. Se vuoi usare lo stesso standard come ETH, usa il valore 18.

Il costruttore

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 }
Mostra tutto
Copia

Il costruttore viene chiamato alla prima creazione del contratto. Per convenzione, i parametri della funzione sono denominati <something>_.

Funzioni dell'interfaccia utente

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. This is the value {ERC20} uses, unless {_setupDecimals} is
23 * called.
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 }
Mostra tutto
Copia

Queste funzioni, name, symbol, e decimals aiutano le interfacce utente a conoscere il tuo contratto, in modo da visualizzarlo correttamente.

Il tipo di restituzione è string memory, a significare che la restituzione è una stringa archiviata in memoria. Le variabili, come le stringhe, sono memorizzabili in tre posizioni:

DurataAccesso al contrattoCosto del Gas
MemoriaChiamata della funzioneLettura/ScritturaDecine o centinaia (maggiori per maggiori posizioni)
CalldataChiamata della funzioneSola LetturaInutilizzabile come tipo di restituzione, solo un tipo di parametro della funzione
ArchiviazioneFino alla modificaLettura/ScritturaElevato (800 per la lettura, 20.000 per la scrittura)

In questo caso, memory è la scelta migliore.

Informazioni di lettura del token

Queste sono funzioni che forniscono informazioni sul token, la fornitura totale o il saldo di un conto.

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

La funzione totalSupply restituisce la fornitura totale di token.

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

Leggi il saldo di un conto. Nota che chiunque può ottenere il saldo del conto di qualcun altro. Non ha senso provare a nascondere queste informazioni, perché sono comunque disponibili su ogni nodo. Non ci sono segreti sulla blockchain.

Trasferire token

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) {
Mostra tutto
Copia

La funzione transfer è chiamata per trasferire i token dal conto del mittente a un altro. Nota che anche se viene restituito un valore booleano, quel valore è sempre true. Se il trasferimento fallisce, il contratto ripristina la chiamata.

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

La funzione _transfer fa il lavoro effettivo. È una funzione privata, chiamabile solo da altre funzioni del contratto. Per convenzione le funzioni private sono denominate _<something>, come le variabili di stato.

Normalmente, in Solidity usiamo msg.sender per il mittente del messaggio. Tuttavia, ciò corrompe OpenGSN(opens in a new tab). Se vogliamo consentire transazioni senza ether con il nostro token, dobbiamo usare _msgSender(). Restituisce msg.sender per le transazioni normali, ma per quelle senza ether restituisce il firmatario originale e non il contratto che ha trasmesso il messaggio.

Funzioni di tolleranza

Sono le funzioni che implementano il margine di tolleranza: allowance, approve, transferFrom e _approve. Inoltre, l'implementazione di OpenZeppelin va oltre lo standard di base e include alcune funzionalità che migliorano la sicurezza: increaseAllowance e decreaseAllowance.

La funzione di tolleranza

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 }
Copia

La funzione allowance consente a chiunque di verificare qualsiasi margine di tolleranza.

La funzione di approvazione

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) {
Copia

Questa funzione viene chiamata per creare un margine di tolleranza. È simile alla suddetta funzione transfer:

  • La funzione chiama semplicemente una funzione interna (in questo caso, _approve), che fa il lavoro vero e proprio.
  • La funzione restituisce true (se va a buon fine), altrimenti si ripristina.
1 _approve(_msgSender(), spender, amount);
2 return true;
3 }
Copia

Usiamo le funzioni interne per minimizzre il numero di posti in cui si verificano cambi di stato. Qualsiasi funzione che cambia stato costituisce un potenziale rischio di sicurezza, che va controllato per sicurezza. Così facendo riduciamo il rischio di conseguenze negative.

La funzione transferFrom

È la funzione chiamata dallo spender per spendere un margine di tolleranza. Richiede due operazioni: trasferire l'importo speso e ridurre il margine di tolleranza in misura pari allo stesso importo.

1 /**
2 * @dev See {IERC20-transferFrom}.
3 *
4 * Emits an {Approval} event indicating the updated allowance. This is not
5 * required by the EIP. See the note at the beginning of {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);
Mostra tutto
Copia

La funzione a.sub(b, "message") produce due azioni. Innanzi tutto calcola a-b, ovvero il nuovo margine di tolleranza (allowance). Quindi controlla che tale risultato non sia negativo. Se lo è, la chiamata si ripristina con il messaggio fornito. Occorre notare che, in caso di ripristino, qualsiasi elaborazione effettuata in precedenza durante tale chiamata viene ignorata, così da evitare di dover annullare il _transfer.

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

Aggiunte di sicurezza di OpenZeppelin

È pericoloso impostare un margine di tolleranza diverso da zero su un altro valore diverso da zero, perché puoi controllare solo l'ordine delle tue transazioni, ma non di quelle altrui. Immagina che ci siano due utenti: Alice, una ragazza ingenua, e Bill, un uomo disonesto. Alice vuole ricevere da Bill un servizio che secondo lei costa cinque token, quindi concede a Bill un margine di tolleranza di cinque token.

Poi qualcosa cambia e il prezzo di Bill aumenta a dieci token. Alice, che è ancora interessata a ricevere il servizio, invia una transazione che imposta il margine di tolleranza di Bill a dieci. Quando Bill vede questa nuova transazione nel pool della transazione, invia una transazione che spende cinque token di Alice e ha un prezzo del gas molto maggiore, così che sarà minata più rapidamente. In questo modo Bill può spendere prima i cinque token e poi, una volta minato il nuovo margine di tolleranza di Alice, spenderne altri dieci per un prezzo complessivo di quindici token, più di quanto Alice volesse autorizzare. Questa tecnica è detta front-running(opens in a new tab)

Transazione di AliceNonce di AliceTransazione di BillNonce di BillTolleranza di BillEntrate totali di Bill da Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

Per evitare questo problema, queste due funzioni (increaseAllowance e decreaseAllowance) ti consentono di modificare il margine di tolleranza di un importo specifico. Quindi, se Bill avesse già speso cinque token, potrà spenderne solo altri cinque. A seconda delle tempistiche, esistono due modi in cui questo può funzionare, entrambi terminano con Bill che riceve solo dieci token:

A:

Transazione di AliceNonce di AliceTransazione di BillNonce di BillTolleranza di BillEntrate totali di Bill da Alice
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

B:

Transazione di AliceNonce di AliceTransazione di BillNonce di BillTolleranza di BillEntrate totali di Bill da 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 }
Mostra tutto
Copia

La funzione a.add(b) è un'aggiunta sicura. Nell'improbabile caso in cui a+b>=2^256, non ha luogo il mormale avvolgimento come avviene con l'addizione normale.

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 }
Mostra tutto
Copia

Funzioni che modificano le informazioni del token

Queste sono le quattro funzioni che effettuano il lavoro effettivo: _transfer, _mint, _burn e _approve.

La funzione _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 {
Mostra tutto
Copia

Questa funzione, _transfer, trasferisce i token da un conto all'altro. È chiamata sia da transfer (per i trasferimenti dal conto del mittente) che da transferFrom (per usare le indennità per trasferire dal conto di qualcun altro).

1 require(sender != address(0), "ERC20: transfer from the zero address");
2 require(recipient != address(0), "ERC20: transfer to the zero address");
Copia

Nessuno possiedere realmente l'indirizzo zero in Ethereum (ciò significa che nessuno conosce una chiave privata la cui chiave pubblica corrispondente è trasformata all'indirizzo zero). Quando si usa quell'indirizzo, in genere si tratta di un bug del software, quindi falliamo se usiamo l'indirizzo zero come mittente o destinatario.

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

Esistono due modi per usare questo contratto:

  1. Usarlo come modello per il proprio codice
  2. Ereditare da esso(opens in a new tab) e sovrascrivere solo le funzioni da modificare

Il secondo metodo è di gran lunga migliore perché il codice ERC-20 di OpenZeppelin è stato già controllato e mostrato come sicuro. Quando si usa l'eredità, le funzioni da modificare sono note e, per fidarsi del tuo contratto, gli altri devono controllare solo quelle funzioni specifiche.

Spesso è utile eseguire una funzione ogni volta che i token passano di mano. Tuttavia, _transfer è una funzione davvero importante ed esiste il rischio di scriverla in modo non sicuro (vedi sotto), quindi è meglio non sovrascriverla. La soluzione è _beforeTokenTransfer, una funzione hook(opens in a new tab). Puoi sovrascrivere questa funzione, che verrà quindi richiamata a ogni trasferimento.

1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
2 _balances[recipient] = _balances[recipient].add(amount);
Copia

Queste sono le righe che effettuano concretamente il trasferimento. Nota che non c'è nulla tra di esse e che sottraiamo l'importo trasferito dal mittente prima di aggiungerlo al destinatario. Questo passaggio è importante perché, se ci fosse una chiamata a un contratto diverso nel mezzo, potrebbe essere utilizzata per barare su questo contratto. Questo metodo di trasferimento è atomico, in quanto nulla può verificarsi mentre è in corso.

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

Infine, emetti un evento Transfer. Gli eventi non sono accessibili agli smart contract, ma il codice eseguito al di fuori della blockchain può ascoltarli e reagire a essi. Ad esempio, un portafoglio può tracciare la ricezione di altri token da parte del proprietario.

Le funzioni _mint e _burn {#_mint-and-_burn}

Queste due funzioni (_mint e _burn) modificano la fornitura totale di token. Sono interne e non esiste alcuna funzione che le chiami in questo contratto, quindi sono utili solo se erediti dal contratto e aggiungi la tua logica per decidere a quali condizioni coniare nuovi token o bruciare quelli esistenti.

NOTA: Ogni token ERC-20 ha la propria logica commerciale che detta la gestione del token. Ad esempio, un contratto di fornitura fissa potrebbe chiamare solo _mint nel costruttore e mai _burn. Un contratto che vende token chiamerà _mint quando è pagato e chiamerà presumibilmente _burn a un certo punto per evitare l'inflazione incontrollata.

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 }
Mostra tutto
Copia

Assicurati di aggiornare _totalSupply quando il numero totale di token cambia.

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 }
Mostra tutto

La funzione _burn è quasi identica a _mint, con la differenza che va in senso opposto.

La funzione _approve {#_approve}

Questa è la funzione che specifica concretamente i margini di tolleranza. Nota che consente a un proprietario di specificare una tolleranza superiore al saldo corrente del proprietario. Questo non è un problema, poiché il saldo è controllato al momento del trasferimento, quando potrebbe differire dal saldo alla creazione del margine di tolleranza.

1 /**
2 * @dev Sets `amount` as the allowance of `spender` over the caller'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;
Mostra tutto
Copia

Emette un evento Approval. In base a come è scritta l'applicazione, il contratto dello spender può essere informato dell'approvazione dal proprietario o da un server che monitora questi eventi.

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

Modificare la variabile dei decimali

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. Most
7 * applications that interact with token contracts will not expect
8 * {decimals} to ever change, and may work incorrectly if it does.
9 */
10 function _setupDecimals(uint8 decimals_) internal {
11 _decimals = decimals_;
12 }
Mostra tutto
Copia

Questa funzione modifica la variabile _decimals, usata per indicare alle interfacce utente come interpretare l'importo. Suggeriamo di chiamarla dal costruttore. Sarebbe disonesto chiamarla in qualsiasi punto successivo e le applicazioni non sono progettate per gestirla.

Hook

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}
Mostra tutto
Copia

Questa è la funzione hook da chiamare durante i trasferimenti. Qui è vuota, ma se hai bisogno di fare qualcosa, basta sovrascriverla.

Conclusioni

A titolo di ripasso, ecco alcune delle idee più importanti in questo contrato (a mio parere, probabilmente il tuo sarà diverso):

  • Non ci sono segreti sulla blockchain. Ogni informazione accessibile a uno smart contract è disponibile per il mondo intero.
  • Puoi controllare l'ordine delle tue transazioni, ma non come si verificano quelle altrui. Per questo modificare un'indennità può esser pericoloso, perché consente allo spender di spendere la somma di entrambi i margini di tolleranza.
  • I valori di tipo uint256 si avvolgono o, in altre parole, 0-1=2^256-1. Se questo non è il comportamento desiderato, devi verificarlo (o usare la libreria SafeMath più adatta alle tue esigenze). Nota che ciò è cambiato in Solidity 0.8.0(opens in a new tab).
  • Effettua tutte le modifiche di un tipo specifico in un luogo specifico, così da semplificare i controlli. Per questo abbiamo, ad esempio, _approve, chiamata da approve, transferFrom, increaseAllowance e decreaseAllowance
  • I cambi di stato dovrebbero essere atomici, senza altre azioni nel mezzo (come si può vedere in _transfer). Questo perché durante il cambio di stato hai uno stato incoerente. Ad esempio, tra il momento in cui detrai dal saldo del mittente e il momento in cui aggiungi al saldo del destinatario, esistono meno token di quanti dovrebbero essercene. Questa condizione potrebbe essere esposta ad abusi se vengono effettuate operazioni tra di essi, specialmente nel caso di chiamate a un contratto differente.

Ora che hai visto come è scritto il contratto ERC-20 di OpenZeppelin e specialmente come viene reso più sicuro, vai a scrivere i tuoi contratti sicuri e le tue applicazioni.

Ultima modifica: @Herbie_23(opens in a new tab), 15 agosto 2023

Questo tutorial è stato utile?