Vai al contenuto principale

Guida dettagliata al contratto ERC-20

Solidity
erc-20
Principiante
Ori Pomerantz
9 marzo 2021
28 minuti di lettura

Introduzione

Uno degli usi più comuni di Ethereum è la creazione da parte di un gruppo di un token scambiabile, in un certo senso la propria valuta. Questi token seguono tipicamente uno standard, l'ERC-20. Questo standard rende possibile scrivere strumenti, come pool di liquidità e portafogli, che funzionano con tutti i token ERC-20. In questo articolo analizzeremo l'implementazione ERC20 in Solidity di OpenZeppelin (opens in a new tab), così come la definizione dell'interfaccia (opens in a new tab).

Questo è codice sorgente annotato. Se vuoi implementare l'ERC-20, leggi questo tutorial (opens in a new tab).

L'Interfaccia

Lo scopo di uno standard come l'ERC-20 è consentire molte implementazioni di token che siano interoperabili tra le applicazioni, come portafogli ed exchange decentralizzati. Per ottenere ciò, creiamo un'interfaccia (opens in a new tab). Qualsiasi codice che debba utilizzare il contratto del token può usare le stesse definizioni nell'interfaccia ed essere compatibile con tutti i contratti dei token che la utilizzano, che si tratti di un portafoglio come MetaMask, di una dApp come etherscan.io o di un contratto diverso come una pool di liquidità.

Illustrazione dell'interfaccia ERC-20

Se sei un programmatore esperto, probabilmente ricordi di aver visto costrutti simili in Java (opens in a new tab) o persino nei file header del C (opens in a new tab).

Questa è una definizione dell'Interfaccia ERC-20 (opens in a new tab) di OpenZeppelin. È una traduzione dello standard leggibile dall'uomo (opens in a new tab) in codice Solidity. Naturalmente, l'interfaccia stessa non definisce come fare qualcosa. Questo è spiegato nel codice sorgente del contratto di seguito.

 

1// SPDX-License-Identifier: MIT

I file Solidity dovrebbero includere un identificatore di licenza. Puoi vedere l'elenco delle licenze qui (opens in a new tab). Se hai bisogno di una licenza diversa, spiegalo semplicemente nei commenti.

 

1pragma solidity >=0.6.0 <0.8.0;

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)). Pertanto, è 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 Interfaccia dello standard ERC20 come definito nell'EIP. */
3
4
5

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

 

1interface IERC20 {

Per convenzione, i nomi delle interfacce iniziano con I.

 

1 /* *
2 * @dev Restituisce la quantità di token esistenti. */
3
4
5
6 function totalSupply() external view returns (uint256);

Questa funzione è external, il che significa che può essere chiamata solo dall'esterno del contratto (opens in a new tab). Restituisce l'offerta totale di token nel contratto. Questo valore viene restituito utilizzando il tipo più comune in Ethereum, 256 bit senza segno (256 bit è la dimensione della parola nativa della EVM). Questa funzione è anche una view, il che significa che non modifica lo stato, quindi può essere eseguita su un singolo nodo invece di farla eseguire a ogni nodo della blockchain. Questo tipo di funzione non genera una transazione e non costa gas.

Nota: In teoria potrebbe sembrare che il creatore di un contratto possa imbrogliare restituendo un'offerta totale inferiore al valore reale, facendo apparire ogni token più prezioso di quanto non sia in realtà. Tuttavia, questo timore ignora la vera natura della blockchain. Tutto ciò che accade sulla blockchain può essere verificato da ogni nodo. Per ottenere ciò, il codice in linguaggio macchina e l'archiviazione di ogni contratto sono disponibili su ogni nodo. Sebbene non sia richiesto di pubblicare il codice Solidity per il tuo contratto, nessuno ti prenderebbe sul serio a meno che tu non pubblichi il codice sorgente e la versione di Solidity con cui è stato compilato, in modo che possa essere verificato rispetto al codice in linguaggio macchina che hai fornito. Ad esempio, vedi questo contratto (opens in a new tab).

 

1 /* *
2 * @dev Restituisce la quantità di token posseduti da `account`. */
3
4
5
6 function balanceOf(address account) external view returns (uint256);

Come dice il nome, balanceOf restituisce il saldo di un account. Gli account di Ethereum sono identificati in Solidity utilizzando il tipo address, che contiene 160 bit. È anche external e view.

 

1 /* *
2 * @dev Sposta `amount` token dall'account del chiamante a `recipient`.
3 *
4 * Restituisce un valore booleano che indica se l'operazione ha avuto successo.
5 *
6 * Emette un evento {Transfer}. */
7
8
9
10
11
12
13
14 function transfer(address recipient, uint256 amount) external returns (bool);
Mostra tutto

La funzione transfer trasferisce dei token dal chiamante a un indirizzo diverso. Ciò comporta un cambiamento di stato, quindi non è una view. Quando un utente chiama questa funzione, crea una transazione e costa gas. Emette anche 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. Tipicamente l'utente invia una transazione e non aspetta una risposta, che potrebbe richiedere un tempo indefinito. L'utente può vedere cosa è successo cercando la ricevuta della transazione (che è identificata dall'hash della transazione) o cercando l'evento Transfer.
  • Altri contratti, che chiamano la funzione come parte di una transazione complessiva. Quei contratti ottengono il risultato immediatamente, perché vengono eseguiti nella stessa transazione, quindi possono utilizzare il valore di ritorno della funzione.

Lo stesso tipo di output viene creato dalle altre funzioni che modificano lo stato del contratto.

 

Le autorizzazioni (allowance) permettono a un account di spendere alcuni token che appartengono a un proprietario diverso. Questo è utile, ad esempio, per i contratti che agiscono come venditori. I contratti non possono monitorare gli eventi, quindi se un acquirente trasferisse i token direttamente al contratto del venditore, quel contratto non saprebbe di essere stato pagato. Invece, l'acquirente autorizza il contratto del venditore a spendere un certo importo, e il venditore trasferisce quell'importo. Questo viene fatto tramite una funzione chiamata dal contratto del venditore, in modo che il contratto del venditore possa sapere se ha avuto successo.

1 /* *
2 * @dev Restituisce il numero rimanente di token che `spender` sarà
3 * autorizzato a spendere per conto di `owner` tramite {transferFrom}. Questo è
4 * zero per impostazione predefinita.
5 *
6 * Questo valore cambia quando vengono chiamati {approve} o {transferFrom}. */
7
8
9
10
11
12
13
14 function allowance(address owner, address spender) external view returns (uint256);
Mostra tutto

La funzione allowance consente a chiunque di interrogare per vedere qual è l'autorizzazione che un indirizzo (owner) consente a un altro indirizzo (spender) di spendere.

 

1 /* *
2 * @dev Imposta `amount` come limite di spesa di `spender` sui token del chiamante.
3 *
4 * Restituisce un valore booleano che indica se l'operazione ha avuto successo.
5 *
6 * IMPORTANTE: Attenzione che modificare un limite di spesa con questo metodo comporta il rischio
7 * che qualcuno possa utilizzare sia il vecchio che il nuovo limite di spesa a causa di uno sfortunato
8 * ordinamento delle transazioni. Una possibile soluzione per mitigare questa race
9 * condition è ridurre prima il limite di spesa dello spender a 0 e impostare il
10 * valore desiderato in seguito:
11 * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
12 *
13 * Emette un evento {Approval}. */
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 function approve(address spender, uint256 amount) external returns (bool);
Mostra tutto

La funzione approve crea un'autorizzazione. Assicurati di leggere il messaggio su come può essere abusata. In Ethereum controlli l'ordine delle tue transazioni, ma non puoi controllare l'ordine in cui verranno eseguite le transazioni di altre persone, a meno che tu non invii la tua transazione finché non vedi che la transazione dell'altra parte è avvenuta.

 

1 /* *
2 * @dev Sposta `amount` token da `sender` a `recipient` utilizzando il
3 * meccanismo del limite di spesa. `amount` viene quindi detratto dal limite di spesa
4 * del chiamante.
5 *
6 * Restituisce un valore booleano che indica se l'operazione ha avuto successo.
7 *
8 * Emette un evento {Transfer}. */
9
10
11
12
13
14
15
16
17
18 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
Mostra tutto

Infine, transferFrom viene utilizzata dallo spenditore per spendere effettivamente l'autorizzazione.

 

1
2 /* *
3 * @dev Emesso quando `value` token vengono spostati da un account (`from`) a
4 * un altro (`to`).
5 *
6 * Nota che `value` può essere zero. */
7
8
9
10
11
12
13 event Transfer(address indexed from, address indexed to, uint256 value);
14
15 /* *
16 * @dev Emesso quando il limite di spesa di uno `spender` per un `owner` viene impostato da
17 * una chiamata a {approve}. `value` è il nuovo limite di spesa. */
18
19
20
21
22 event Approval(address indexed owner, address indexed spender, uint256 value);
23}
Mostra tutto

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

Il Contratto Effettivo

Questo è il contratto effettivo che implementa lo standard ERC-20, preso da qui (opens in a new tab). Non è pensato per essere utilizzato così com'è, ma puoi ereditare (opens in a new tab) da esso per estenderlo a qualcosa di utilizzabile.

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

 

Dichiarazioni di Importazione

Oltre alle definizioni dell'interfaccia di cui sopra, la definizione del contratto importa altri due file:

1
2import "../../GSN/Context.sol";
3import "./IERC20.sol";
4import "../../math/SafeMath.sol";
  • GSN/Context.sol contiene le definizioni necessarie per utilizzare OpenGSN (opens in a new tab), un sistema che consente agli utenti senza ether di utilizzare la blockchain. Nota che questa è una vecchia versione, se vuoi integrarti con OpenGSN usa questo tutorial (opens in a new tab).
  • La libreria SafeMath (opens in a new tab), che previene overflow/underflow aritmetici per le versioni di Solidity <0.8.0. In Solidity ≥0.8.0, le operazioni aritmetiche si annullano automaticamente in caso di overflow/underflow, rendendo SafeMath non necessaria. Questo contratto utilizza SafeMath per la retrocompatibilità con le versioni precedenti del compilatore.

 

Questo commento spiega lo scopo del contratto.

1/* *
2 * @dev Implementazione dell'interfaccia {IERC20}.
3 *
4 * Questa implementazione è agnostica rispetto al modo in cui vengono creati i token. Ciò significa
5 * che un meccanismo di fornitura deve essere aggiunto in un contratto derivato utilizzando {_mint}.
6 * Per un meccanismo generico vedi {ERC20PresetMinterPauser}.
7 *
8 * SUGGERIMENTO: Per una descrizione dettagliata vedi la nostra guida
9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[How
10 * to implement supply mechanisms].
11 *
12 * Abbiamo seguito le linee guida generali di OpenZeppelin: le funzioni si annullano (revert) invece
13 * di restituire `false` in caso di fallimento. Questo comportamento è tuttavia convenzionale
14 * e non è in conflitto con le aspettative delle applicazioni ERC20.
15 *
16 * Inoltre, un evento {Approval} viene emesso alle chiamate a {transferFrom}.
17 * Questo permette alle applicazioni di ricostruire il limite di spesa per tutti gli account semplicemente
18 * ascoltando tali eventi. Altre implementazioni dell'EIP potrebbero non emettere
19 * questi eventi, poiché non è richiesto dalle specifiche.
20 *
21 * Infine, le funzioni non standard {decreaseAllowance} e {increaseAllowance}
22 * sono state aggiunte per mitigare i ben noti problemi relativi all'impostazione
23 * dei limiti di spesa. Vedi {IERC20-approve}. */
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Mostra tutto

Definizione del Contratto

1contract ERC20 is Context, IERC20 {

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

 

1
2 using SafeMath for uint256;
3

Questa riga collega 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 private, ma ciò significa solo che altri contratti sulla blockchain non possono leggerle. Non ci sono segreti sulla blockchain, il software su ogni nodo ha lo stato di ogni contratto a ogni blocco. Per convenzione, le variabili di stato sono chiamate _<qualcosa>.

Le prime due variabili sono mappature (mapping) (opens in a new tab), il che significa che si comportano all'incirca come gli array associativi (opens in a new tab), tranne per il fatto che le chiavi sono valori numerici. L'archiviazione viene allocata solo per le voci che hanno valori diversi da quello predefinito (zero).

1 mapping (address => uint256) private _balances;

La prima mappatura, _balances, rappresenta gli indirizzi e i loro rispettivi saldi di questo token. Per accedere al saldo, usa questa sintassi: _balances[<indirizzo>].

 

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

Questa variabile, _allowances, memorizza le autorizzazioni spiegate in precedenza. Il primo indice è il proprietario dei token, e il secondo è il contratto con l'autorizzazione. Per accedere all'importo che l'indirizzo A può spendere dall'account dell'indirizzo B, usa _allowances[B][A].

 

1 uint256 private _totalSupply;

Come suggerisce il nome, questa variabile tiene traccia dell'offerta totale di token.

 

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

Queste tre variabili sono utilizzate per migliorare la leggibilità. Le prime due si spiegano da sole, ma _decimals no.

Da un lato, Ethereum non ha variabili a virgola mobile o frazionarie. Dall'altro lato, agli esseri umani piace poter dividere i token. Uno dei motivi per cui le persone hanno scelto l'oro come valuta è che era difficile dare il resto quando qualcuno voleva comprare una mucca per il valore di un'anatra.

La soluzione è tenere traccia dei numeri interi, ma contare invece del token reale un token frazionario che è quasi senza valore. Nel caso dell'ether, il token frazionario si chiama wei, e 10^18 wei equivalgono a un ETH. Al momento della stesura, 10.000.000.000.000 wei corrispondono a circa un centesimo di dollaro USA o di euro.

Le applicazioni devono sapere come visualizzare il saldo del token. Se un utente ha 3.141.000.000.000.000.000 wei, sono 3,14 ETH? 31,41 ETH? 3.141 ETH? Nel caso dell'ether è definito 10^18 wei per ETH, ma per il tuo token puoi selezionare un valore diverso. Se dividere il token non ha senso, puoi usare un valore _decimals pari a zero. Se vuoi usare lo stesso standard dell'ETH, usa il valore 18.

Il Costruttore

1 /* *
2 * @dev Imposta i valori per {name} e {symbol}, inizializza {decimals} con
3 * un valore predefinito di 18.
4 *
5 * Per selezionare un valore diverso per {decimals}, usa {_setupDecimals}.
6 *
7 * Tutti e tre questi valori sono immutabili: possono essere impostati solo una volta durante
8 * la costruzione. */
9
10
11
12
13
14
15
16
17
18 constructor (string memory name_, string memory symbol_) public {
19 // In Solidity ≥0.7.0, 'public' è implicito e può essere omesso.
20
21 _name = name_;
22 _symbol = symbol_;
23 _decimals = 18;
24 }
Mostra tutto

Il costruttore viene chiamato quando il contratto viene creato per la prima volta. Per convenzione, i parametri della funzione sono chiamati <qualcosa>_.

Funzioni dell'Interfaccia Utente

1 /* *
2 * @dev Restituisce il nome del token. */
3
4
5
6 function name() public view returns (string memory) {
7 return _name;
8 }
9
10 /* *
11 * @dev Restituisce il simbolo del token, di solito una versione più corta del
12 * nome. */
13
14
15
16
17 function symbol() public view returns (string memory) {
18 return _symbol;
19 }
20
21 /* *
22 * @dev Restituisce il numero di decimali utilizzati per ottenere la sua rappresentazione utente.
23 * Ad esempio, se `decimals` è uguale a `2`, un saldo di `505` token dovrebbe
24 * essere mostrato a un utente come `5,05` (`505 / 10 ** 2`).
25 *
26 * I token di solito optano per un valore di 18, imitando la relazione tra
27 * ether e wei. Questo è il valore che {ERC20} utilizza, a meno che non venga chiamato
28 * {_setupDecimals}.
29 *
30 * NOTA: Questa informazione è utilizzata solo a scopo di _visualizzazione_: non
31 * influisce in alcun modo sull'aritmetica del contratto, inclusi
32 * {IERC20-balanceOf} e {IERC20-transfer}. */
33
34
35
36
37
38
39
40
41
42
43
44
45
46 function decimals() public view returns (uint8) {
47 return _decimals;
48 }
Mostra tutto

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

Il tipo di ritorno è string memory, il che significa che restituisce una stringa memorizzata in memoria. Le variabili, come le stringhe, possono essere memorizzate in tre posizioni:

DurataAccesso al ContrattoCosto del Gas
Memoria (Memory)Chiamata di funzioneLettura/ScritturaDecine o centinaia (più alto per posizioni più alte)
Dati di chiamata (Calldata)Chiamata di funzioneSolo LetturaNon può essere usato come tipo di ritorno, solo come tipo di parametro di funzione
Archiviazione (Storage)Fino a modificaLettura/ScritturaAlto (800 per la lettura, 20k per la scrittura)

In questo caso, memory è la scelta migliore.

Leggere le Informazioni del Token

Queste sono funzioni che forniscono informazioni sul token, che si tratti dell'offerta totale o del saldo di un account.

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

La funzione totalSupply restituisce l'offerta totale di token.

 

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

Legge il saldo di un account. Nota che a chiunque è permesso ottenere il saldo dell'account di chiunque altro. Non ha senso cercare di nascondere queste informazioni, perché sono comunque disponibili su ogni nodo. Non ci sono segreti sulla blockchain.

Trasferire Token

1 /* *
2 * @dev Vedi {IERC20-transfer}.
3 *
4 * Requisiti:
5 *
6 * - `recipient` non può essere l'indirizzo zero.
7 * - il chiamante deve avere un saldo di almeno `amount`. */
8
9
10
11
12
13
14
15
16 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {
Mostra tutto

La funzione transfer viene chiamata per trasferire token dall'account del mittente a uno diverso. Nota che anche se restituisce un valore booleano, quel valore è sempre true. Se il trasferimento fallisce, il contratto annulla (revert) la chiamata.

 

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

La funzione _transfer fa il lavoro effettivo. È una funzione privata che può essere chiamata solo da altre funzioni del contratto. Per convenzione le funzioni private sono chiamate _<qualcosa>, come le variabili di stato.

Normalmente in Solidity usiamo msg.sender per il mittente del messaggio. Tuttavia, questo rompe 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 inoltrato il messaggio.

Funzioni di Autorizzazione

Queste sono le funzioni che implementano la funzionalità di autorizzazione: allowance, approve, transferFrom e _approve. Inoltre, l'implementazione di OpenZeppelin va oltre lo standard di base per includere alcune funzionalità che migliorano la sicurezza: increaseAllowance e decreaseAllowance.

La funzione allowance

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

La funzione allowance consente a tutti di controllare qualsiasi autorizzazione.

La funzione approve

1 /* *
2 * @dev Vedi {IERC20-approve}.
3 *
4 * Requisiti:
5 *
6 * - `spender` non può essere l'indirizzo zero. */
7
8
9
10
11
12
13
14 function approve(address spender, uint256 amount) public virtual override returns (bool) {
Mostra tutto

Questa funzione viene chiamata per creare un'autorizzazione. È simile alla funzione transfer di cui sopra:

  • La funzione chiama semplicemente una funzione interna (in questo caso, _approve) che fa il vero lavoro.
  • La funzione restituisce true (se ha successo) o si annulla (se non lo ha).

 

1 _approve(_msgSender(), spender, amount);
2 return true;
3 }

Usiamo funzioni interne per ridurre al minimo il numero di punti in cui avvengono i cambiamenti di stato. Qualsiasi funzione che modifichi lo stato è un potenziale rischio per la sicurezza che deve essere verificato. In questo modo abbiamo meno possibilità di sbagliare.

La funzione transferFrom

Questa è la funzione che uno spenditore chiama per spendere un'autorizzazione. Ciò richiede due operazioni: trasferire l'importo speso e ridurre l'autorizzazione di tale importo.

1 /* *
2 * @dev Vedi {IERC20-transferFrom}.
3 *
4 * Emette un evento {Approval} che indica il limite di spesa aggiornato. Questo non è
5 * richiesto dall'EIP. Vedi la nota all'inizio di {ERC20}.
6 *
7 * Requisiti:
8 *
9 * - `sender` e `recipient` non possono essere l'indirizzo zero.
10 * - `sender` deve avere un saldo di almeno `amount`.
11 * - il chiamante deve avere un limite di spesa per i token di ``sender`` di almeno
12 * `amount`. */
13
14
15
16
17
18
19
20
21
22
23
24
25
26 function transferFrom(address sender, address recipient, uint256 amount) public virtual
27 override returns (bool) {
28 _transfer(sender, recipient, amount);
Mostra tutto

 

La chiamata di funzione a.sub(b, "message") fa due cose. Primo, calcola a-b, che è la nuova autorizzazione. Secondo, controlla che questo risultato non sia negativo. Se è negativo, la chiamata si annulla con il messaggio fornito. Nota che quando una chiamata si annulla, qualsiasi elaborazione eseguita in precedenza durante quella chiamata viene ignorata, quindi non abbiamo bisogno di annullare il _transfer.

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

Aggiunte di sicurezza di OpenZeppelin

È pericoloso impostare un'autorizzazione diversa da zero a un altro valore diverso da zero, perché controlli solo l'ordine delle tue transazioni, non quello di nessun altro. Immagina di avere due utenti, Alice che è ingenua e Bill che è disonesto. Alice vuole un servizio da Bill, che pensa costi cinque token, quindi dà a Bill un'autorizzazione di cinque token.

Poi qualcosa cambia e il prezzo di Bill sale a dieci token. Alice, che vuole ancora il servizio, invia una transazione che imposta l'autorizzazione di Bill a dieci. Nel momento in cui Bill vede questa nuova transazione nel pool delle transazioni, invia una transazione che spende i cinque token di Alice e ha un prezzo del gas molto più alto in modo che venga minata più velocemente. In questo modo Bill può spendere prima cinque token e poi, una volta minata la nuova autorizzazione di Alice, spenderne altri dieci per un prezzo totale di quindici token, più di quanto Alice intendesse autorizzare. Questa tecnica è chiamata front-running (opens in a new tab)

Transazione di AliceNonce di AliceTransazione di BillNonce di BillAutorizzazione 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) consentono di modificare l'autorizzazione di un importo specifico. Quindi, se Bill avesse già speso cinque token, potrà spenderne solo altri cinque. A seconda delle tempistiche, ci sono due modi in cui questo può funzionare, entrambi i quali finiscono con Bill che ottiene solo dieci token:

A:

Transazione di AliceNonce di AliceTransazione di BillNonce di BillAutorizzazione 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 BillAutorizzazione 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 Aumenta atomicamente il limite di spesa concesso a `spender` dal chiamante.
3 *
4 * Questa è un'alternativa a {approve} che può essere utilizzata come mitigazione per
5 * i problemi descritti in {IERC20-approve}.
6 *
7 * Emette un evento {Approval} che indica il limite di spesa aggiornato.
8 *
9 * Requisiti:
10 *
11 * - `spender` non può essere l'indirizzo zero. */
12
13
14
15
16
17
18
19
20
21
22
23
24 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
25 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));
26 return true;
27 }
Mostra tutto

La funzione a.add(b) è un'addizione sicura. Nel caso improbabile in cui a+b>=2^256, non ricomincia da zero (wrap around) come fa la normale addizione.

1
2 /* *
3 * @dev Diminuisce atomicamente il limite di spesa concesso a `spender` dal chiamante.
4 *
5 * Questa è un'alternativa a {approve} che può essere utilizzata come mitigazione per
6 * i problemi descritti in {IERC20-approve}.
7 *
8 * Emette un evento {Approval} che indica il limite di spesa aggiornato.
9 *
10 * Requisiti:
11 *
12 * - `spender` non può essere l'indirizzo zero.
13 * - `spender` deve avere un limite di spesa per il chiamante di almeno
14 * `subtractedValue`. */
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
30 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,
31 "ERC20: decreased allowance below zero"));
32 return true;
33 }
Mostra tutto

Funzioni che Modificano le Informazioni del Token

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

La funzione _transfer

1 /* *
2 * @dev Sposta `amount` token da `sender` a `recipient`.
3 *
4 * Questa funzione interna è equivalente a {transfer}, e può essere utilizzata per
5 * es., implementare commissioni automatiche sui token, meccanismi di slashing, ecc.
6 *
7 * Emette un evento {Transfer}.
8 *
9 * Requisiti:
10 *
11 * - `sender` non può essere l'indirizzo zero.
12 * - `recipient` non può essere l'indirizzo zero.
13 * - `sender` deve avere un saldo di almeno `amount`. */
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 function _transfer(address sender, address recipient, uint256 amount) internal virtual {
Mostra tutto

Questa funzione, _transfer, trasferisce token da un account a un altro. Viene chiamata sia da transfer (per i trasferimenti dal proprio account del mittente) sia da transferFrom (per l'utilizzo delle autorizzazioni per trasferire dall'account 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");

Nessuno possiede effettivamente l'indirizzo zero in Ethereum (cioè, nessuno conosce una chiave privata la cui chiave pubblica corrispondente si trasforma nell'indirizzo zero). Quando le persone usano quell'indirizzo, di solito si tratta di un bug del software, quindi falliamo se l'indirizzo zero viene utilizzato come mittente o destinatario.

 

1 _beforeTokenTransfer(sender, recipient, amount);
2

Ci sono due modi per utilizzare questo contratto:

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

Il secondo metodo è molto migliore perché il codice ERC-20 di OpenZeppelin è già stato verificato e si è dimostrato sicuro. Quando usi l'ereditarietà è chiaro quali sono le funzioni che modifichi, e per fidarsi del tuo contratto le persone devono solo verificare quelle funzioni specifiche.

Spesso è utile eseguire una funzione ogni volta che i token passano di mano. Tuttavia, _transfer è una funzione molto importante ed è possibile 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 e verrà chiamata a ogni trasferimento.

 

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

Queste sono le righe che eseguono effettivamente il trasferimento. Nota che non c'è nulla tra di esse, e che sottraiamo l'importo trasferito dal mittente prima di aggiungerlo al destinatario. Questo è importante perché se ci fosse stata una chiamata a un contratto diverso nel mezzo, avrebbe potuto essere usata per imbrogliare questo contratto. In questo modo il trasferimento è atomico, non può succedere nulla nel mezzo.

 

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

Infine, emette un evento Transfer. Gli eventi non sono accessibili ai contratti intelligenti, ma il codice in esecuzione all'esterno della blockchain può ascoltare gli eventi e reagire ad essi. Ad esempio, un portafoglio può tenere traccia di quando il proprietario ottiene più token.

Le funzioni _mint e _burn

Queste due funzioni (_mint e _burn) modificano l'offerta totale di token. Sono interne e non c'è 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 di business che detta la gestione dei token. Ad esempio, un contratto a fornitura fissa potrebbe chiamare _mint solo nel costruttore e non chiamare mai _burn. Un contratto che vende token chiamerà _mint quando viene pagato, e presumibilmente chiamerà _burn a un certo punto per evitare un'inflazione fuori controllo.

1 /* * @dev Crea `amount` token e li assegna a `account`, aumentando
2 * la fornitura totale.
3 *
4 * Emette un evento {Transfer} con `from` impostato all'indirizzo zero.
5 *
6 * Requisiti:
7 *
8 * - `to` non può essere l'indirizzo zero. */
9
10
11
12
13
14
15
16
17
18 function _mint(address account, uint256 amount) internal virtual {
19 require(account != address(0), "ERC20: mint to the zero address");
20 _beforeTokenTransfer(address(0), account, amount);
21 _totalSupply = _totalSupply.add(amount);
22 _balances[account] = _balances[account].add(amount);
23 emit Transfer(address(0), account, amount);
24 }
Mostra tutto

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

 

1 /* *
2 * @dev Distrugge `amount` token da `account`, riducendo la
3 * fornitura totale.
4 *
5 * Emette un evento {Transfer} con `to` impostato all'indirizzo zero.
6 *
7 * Requisiti:
8 *
9 * - `account` non può essere l'indirizzo zero.
10 * - `account` deve avere almeno `amount` token. */
11
12
13
14
15
16
17
18
19
20
21
22 function _burn(address account, uint256 amount) internal virtual {
23 require(account != address(0), "ERC20: burn from the zero address");
24
25 _beforeTokenTransfer(account, address(0), amount);
26
27 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");
28 _totalSupply = _totalSupply.sub(amount);
29 emit Transfer(account, address(0), amount);
30 }
Mostra tutto

La funzione _burn è quasi identica a _mint, tranne per il fatto che va nell'altra direzione.

La funzione _approve

Questa è la funzione che specifica effettivamente le autorizzazioni. Nota che consente a un proprietario di specificare un'autorizzazione superiore al saldo attuale del proprietario. Questo va bene perché il saldo viene controllato al momento del trasferimento, quando potrebbe essere diverso dal saldo al momento della creazione dell'autorizzazione.

1 /* *
2 * @dev Imposta `amount` come limite di spesa di `spender` sui token di `owner`.
3 *
4 * Questa funzione interna è equivalente a `approve`, e può essere utilizzata per
5 * es., impostare limiti di spesa automatici per determinati sottosistemi, ecc.
6 *
7 * Emette un evento {Approval}.
8 *
9 * Requisiti:
10 *
11 * - `owner` non può essere l'indirizzo zero.
12 * - `spender` non può essere l'indirizzo zero. */
13
14
15
16
17
18
19
20
21
22
23
24
25
26 function _approve(address owner, address spender, uint256 amount) internal virtual {
27 require(owner != address(0), "ERC20: approve from the zero address");
28 require(spender != address(0), "ERC20: approve to the zero address");
29
30 _allowances[owner][spender] = amount;
Mostra tutto

 

Emette un evento Approval. A seconda di come è scritta l'applicazione, il contratto dello spenditore può essere informato dell'approvazione dal proprietario o da un server che ascolta questi eventi.

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

Modificare la Variabile Decimals

1
2
3 /* *
4 * @dev Imposta {decimals} a un valore diverso da quello predefinito di 18.
5 *
6 * AVVERTENZA: Questa funzione dovrebbe essere chiamata solo dal costruttore. La maggior parte
7 * delle applicazioni che interagiscono con i contratti dei token non si aspetterà
8 * che {decimals} cambi mai, e potrebbero funzionare in modo errato se lo fa. */
9
10
11
12
13
14
15
16 function _setupDecimals(uint8 decimals_) internal {
17 _decimals = decimals_;
18 }
Mostra tutto

Questa funzione modifica la variabile _decimals che viene utilizzata per dire alle interfacce utente come interpretare l'importo. Dovresti chiamarla dal costruttore. Sarebbe disonesto chiamarla in qualsiasi momento successivo, e le applicazioni non sono progettate per gestirlo.

Hook

1
2 /* *
3 * @dev Hook che viene chiamato prima di qualsiasi trasferimento di token. Questo include
4 * il coniare e il bruciare.
5 *
6 * Condizioni di chiamata:
7 *
8 * - quando `from` e `to` sono entrambi non zero, `amount` dei token di ``from``
9 * saranno trasferiti a `to`.
10 * - quando `from` è zero, `amount` token saranno coniati per `to`.
11 * - quando `to` è zero, `amount` dei token di ``from`` saranno bruciati.
12 * - `from` e `to` non sono mai entrambi zero.
13 *
14 * Per saperne di più sugli hook, vai a xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }
30}
Mostra tutto

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

Conclusione

Per riepilogare, ecco alcune delle idee più importanti in questo contratto (secondo me, la tua opinione potrebbe variare):

  • Non ci sono segreti sulla blockchain. Qualsiasi informazione a cui un contratto intelligente può accedere è disponibile per tutto il mondo.
  • Puoi controllare l'ordine delle tue transazioni, ma non quando avvengono le transazioni di altre persone. Questo è il motivo per cui modificare un'autorizzazione può essere pericoloso, perché consente allo spenditore di spendere la somma di entrambe le autorizzazioni.
  • I valori di tipo uint256 ricominciano da zero (wrap around). In altre parole, 0-1=2^256-1. Se questo non è il comportamento desiderato, devi verificarlo (o usare la libreria SafeMath che lo fa per te). Nota che questo è cambiato in Solidity 0.8.0 (opens in a new tab).
  • Esegui tutti i cambiamenti di stato di un tipo specifico in un luogo specifico, perché semplifica la verifica (auditing). Questo è il motivo per cui abbiamo, ad esempio, _approve, che viene chiamata da approve, transferFrom, increaseAllowance e decreaseAllowance
  • I cambiamenti di stato dovrebbero essere atomici, senza nessun'altra azione nel mezzo (come puoi vedere in _transfer). Questo perché durante il cambiamento di stato si ha uno stato incoerente. Ad esempio, tra il momento in cui deduci dal saldo del mittente e il momento in cui aggiungi al saldo del destinatario ci sono meno token in esistenza di quanti dovrebbero esserci. Questo potrebbe essere potenzialmente abusato se ci sono operazioni tra di loro, specialmente chiamate a un contratto diverso.

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

Vedi qui per altri miei lavori (opens in a new tab).

Ultimo aggiornamento della pagina: 22 ottobre 2025

Questo tutorial è stato utile?