Vai al contenuto principale

ERC-20 con barriere di sicurezza

erc-20
Principiante
Ori Pomerantz
15 agosto 2022
8 minuti letti minute read

Introduzione

Una delle cose fantastiche su Ethereum è che non esiste un'autorità centrale che possa modificare o annullare le tue transazioni. Uno dei suoi grandi problemi è che non c'è un'autorità centrale con il potere di annullare gli errori o le transazioni illecite degli utenti. In questo articolo imparerai alcuni dei comuni errori che gli utenti commettono con i token ERC-20, nonché come creare dei contratti ERC-20 che aiutano gli utenti a evitarli, o danno poteri a un'autorità centrale (ad esempio, congelare gli account).

Nota che, mentre utilizzeremo il contratto del token ERC-20 di OpenZeppelin(opens in a new tab), questo articolo non lo spiega nel dettaglio. Puoi trovare queste informazioni qui.

Se desideri visualizzare il codice sorgente completo:

  1. Apri l'IDE di Remix(opens in a new tab).
  2. Clicca l'icona di clonazione di GitHub (clone github icon).
  3. Clona la repository di GitHub https://github.com/qbzzt/20220815-erc20-safety-rails.
  4. Apri contratti > erc20-safety-rails.sol.

Creare un contratto ERC-20

Prima di poter aggiungere la funzionalità della barriera di sicurezza, ci occorre un contratto ERC-20. In questo articolo utilizzeremo la procedura guidata dei contratti di OpenZeppelin(opens in a new tab). Aprila in un altro browser e segui queste istruzioni:

  1. Seleziona ERC20.

  2. Inserisci queste impostazioni:

    ParametroValore
    NomeSafetyRailsToken
    SymbolSAFE
    Premint1000
    CaratteristicheNessuno
    Access ControlOwnable
    UpgradabilityNessuno
  3. Scorri in alto e clicca su Apri su Remix (per Remix) o su Scarica per utilizzare un ambiente differente. Supporrò che tu stia utilizzando Remix, altrimenti, effettua le modifiche appropriate.

  4. Ora, abbiamo un contratto ERC-20 pienamente funzionante. Puoi espandere .deps > npm per visualizzare il codice importato.

  5. Compila, distribuisci e gioca con il contratto, per scoprire che funziona come un contratto ERC-20. Se devi apprendere come funziona Remix, utilizza questo tutorial(opens in a new tab).

Errori comuni

Gli errori

Gli utenti, talvolta, inviano dei token all'indirizzo errato. Anche se non possiamo leggere la loro mente per sapere cosa intendevano fare, ci sono due tipi di errore che capitano spesso e sono facili da rilevare:

  1. Inviare i token all'indirizzo del contratto. Ad esempio, il token OP di Optimism(opens in a new tab) è riuscito ad accumulare oltre 120.000(opens in a new tab) token OP in meno di due mesi. Questo rappresenta una significativa quantità di ricchezza che, presumibilmente, è semplicemente stata persa dalle persone.

  2. Inviare i token a un indirizzo vuoto, non corrispondente a un conto posseduto esternamente o a un contratto intelligente. Sebbene non siano disponibili le statistiche su quanto spesso si verifichi, un incidente potrebbe costare fino a 20.000.000 token(opens in a new tab).

Prevenire i trasferimenti

Il contratto ERC-20 di OpenZeppelin include un hook, _beforeTokenTransfer(opens in a new tab), chiamato prima del trasferimento di un token. Per impostazione predefinita, questo hook non fa nulla, ma possiamo allegarci la nostra funzionalità, come controlli che ripristinano la transazione se si verifica un problema.

Per utilizzare l'hook, aggiungi questa funzione dopo il costruttore:

1 function _beforeTokenTransfer(address from, address to, uint256 amount)
2 internal virtual
3 override(ERC20)
4 {
5 super._beforeTokenTransfer(from, to, amount);
6 }
Copia

Alcune parti di questa funzione potrebbero essere nuove, se non hai familiarità con Solidity:

1 internal virtual
Copia

La parola chiave virtual signiica che appena abbiamo ereditato la funzionalità da ERC20 e sovrascritto questa funzione, gli altri contratti possono ereditare da noi e sovrascriverla.

1 override(ERC20)
Copia

Dobbiamo specificare esplicitamente che stiamo sovrascrivendo(opens in a new tab) la definizione del token ERC20 di _beforeTokenTransfer. In generale, le definizioni esplicite sono molto migliori, dal punto di vista della sicurezza, rispetto a quelle implicite: non puoi dimenticare di aver fatto qualcosa se ce l'hai davanti. Questo è anche il motivo per cui dobbiamo specificare il _beforeTokenTransfer di quale superclasse stiamo sovrascrivendo.

1 super._beforeTokenTransfer(from, to, amount);
Copia

Questa riga chiama la funzione _beforeTokenTransfer del contratto o dei contratti da cui abbiamo ereditato o che la contengono. In qusto caso, è solo ERC20, Ownable non contiene questo hook. Sebbene correntemente ERC20._beforeTokenTransfer non faccia nulla, lo chiamiamo nel caso in cui la funzionalità sia aggiunta in futuro (e, quindi, decidiamo di ridistribuire il contratto, poiché i contratti non cambiano dopo la distribuzione).

Codifica dei requisiti

Vogliamo aggiungere questi requisiti alla funzione:

  • L'indirizzo to non può equivalere ad address(this), l'indirizzo dello stesso contratto ERC-20.
  • L'indirizzo to non può essere vuoto, dev'essere:
    • Account posseduti esternamente (EOA). Non possiamo verificare se un indirizzo è un EOA direttamente, ma possiamo verificare il saldo di ETH di un indirizzo. Gli EOA contengono quasi sempre un saldo, anche se non sono più utilizzati; è difficile ripulirli fino all'ultimo wei.
    • Un contratto intelligente. Testare se un indirizzo è un contratto intelligente è più difficile. Esiste un codice operativo che controlla la lunghezza del codice esterno, chiamato EXTCODESIZE(opens in a new tab), ma non è direttamente disponibile in Solidity. Dobbiamo utilizzare Yul(opens in a new tab), un assemblaggio dell'EVM, per farlo. Esistono altri valori che potremmo utilizzare da Solidity (<address>.code e <address>.codehash(opens in a new tab)), ma costano di più.

Analizziamo il codice, riga per riga:

1 require(to != address(this), "Can't send tokens to the contract address");
Copia

Questo è il primo requisito, controlla che to this(address) non siano la stessa cosa.

1 bool isToContract;
2 assembly {
3 isToContract := gt(extcodesize(to), 0)
4 }
Copia

È così che controlliamo se un indirizzo è un contratto. Non possiamo ricevere direttamente il risultato da Yul, quindi, invece, definiamo una variabile che detenga il risultato (isToContract in questo caso). Yul funziona così: ogni codice operativo è considerato come una funzione. Quindi, prima chiamiamo EXTCODESIZE(opens in a new tab) per ottenere le dimensioni del contratto, quindi utilizziamo GT(opens in a new tab) per verificare che non sia zero (stiamo avendo a che fare con interi non firmati, quindi, ovviamente, non possono essere negativi). Poi, dobbiamo scrivere il risultato in isToContract.

1 require(to.balance != 0 || isToContract, "Can't send tokens to an empty address");
Copia

E, infine, abbiamo il controllo effettivo per gli indirizzi vuoti.

Accesso amministrativo

Talvolta è utile avere un amministratore che possa annullare gli errori. Per ridurre il potenziale di abusi, questo può essere una multifirma(opens in a new tab), quindi più persone devono approvare un'azione. In questo articolo abbiamo due funzionalità amministrative:

  1. Congelamento e scongelamento degli account. Utile, ad esempio, quando un account potrebbe essere compromesso.

  2. Pulizia della risorsa.

    Talvolta, i truffatori inviano token fraudolenti al contratto del token reale, per ottenere legittimità. Ad esempio, vedi qui(opens in a new tab). Il contratto ERC-20 legittimo è 0x4200....0042(opens in a new tab). La truffa che pretende di essere tale contratto è 0x234....bbe(opens in a new tab).

    Inoltre, è possibile che le persone inviino token ERC-20 legittimi al nostro contratto per errore, un altro motivo per cui vogliamo avere un modo per farli uscire.

OpenZeppelin fornisce due meccanismi per consentire l'accesso amministrativo:

Per semplicità, in questo articolo utilizzeremo Ownable.

Congelare e scongelare i contratti

Congelare e scongelare i contratti richiede diverse modifiche:

  • Una mappatura(opens in a new tab) dagli indirizzi a booleani(opens in a new tab) per tenere traccia di quali indirizzi siano congelati. Tutti i valori sono inizialmente pari a zero, che per i valori booleani è interpretato come falso. Questo è ciò che vogliamo perché gli account non sono congelati per impostazione predefinita.

    1 mapping(address => bool) public frozenAccounts;
    Copia
  • Eventi(opens in a new tab) per informare chiunque sia interessato quando un account è congelato o scongelato. Tecnicamente parlando, gli eventi non sono necessari per tali azioni, ma aiutano il codice esterno alla catena a essere capace di ascoltare tali eventi e sapere che sta succedendo. È considerata buona pratica, per un contratto intelligente, di emetterli quando si verifica qualcosa che potrebbe essere rilevante per qualcuno.

    Gli eventi sono indicizzati, quindi sarà possibile cercarli tutte le volte che un account è stato congelato o scongelato.

    1 // When accounts are frozen or unfrozen
    2 event AccountFrozen(address indexed _addr);
    3 event AccountThawed(address indexed _addr);
    Copia
  • Funzioni per congelare e scongelare gli account. Queste due funzioni sono quasi identiche, quindi analizzeremo soltanto la funzione di congelamento.

    1 function freezeAccount(address addr)
    2 public
    3 onlyOwner
    Copia

    Le funzioni contrassegnate come public(opens in a new tab), possono essere chiamate da altri contratti intelligenti o direttamente da una transazione.

    1 {
    2 require(!frozenAccounts[addr], "Account already frozen");
    3 frozenAccounts[addr] = true;
    4 emit AccountFrozen(addr);
    5 } // freezeAccount
    Copia

    Se l'account è già congelato, si ripristina. Altrimenti, lo congela ed emette (emit) un evento.

  • Modificare _beforeTokenTransfer per impedire che il denaro sia spostato da un account congelato. Nota che il denaro è ancora trasferibile in un account congelato.

    1 require(!frozenAccounts[from], "The account is frozen");
    Copia

Pulizia della risorsa

Per rilasciare i token ERC-20 detenuti da questo contratto, dobbiamo chiamare una funzione sul contratto del token cui aappartengono, transfer(opens in a new tab) o approve(opens in a new tab). Non ha senso, in questo caso, sprecare gas sulle indennità, potremmo anche trasferirle direttamente.

1 function cleanupERC20(
2 address erc20,
3 address dest
4 )
5 public
6 onlyOwner
7 {
8 IERC20 token = IERC20(erc20);
Copia

Questa è la sintassi per creare un oggetto per un contratto quando riceviamo l'indirizzo. Possiamo farlo perché abbiamo la definizione per i token ERC20 come parte del codice sorgente (vedi riga 4) e tale file include la definizione per IERC20(opens in a new tab), l'interfaccia per un contratto ERC-20 di OpenZeppelin.

1 uint balance = token.balanceOf(address(this));
2 token.transfer(dest, balance);
3 }
Copia

Questa è una funzione di pulizia, quindi, presumibilmente, non vogliamo lasciare alcun token. Invece di ottenere manualmente il saldo dall'utente, potremmo automatizzare anche questo processo.

Conclusioni

Questa non è una soluzione perfetta, non esiste una soluzione perfetta al problema "un utente ha commesso un errore". Tuttavia, utilizzando questi tipi di controlli, alcuni errori possono almeno essere prevenuti. L'abilità di congelare gli account, sebbene pericolosa, è utilizzabile per limitare i danni di certi attacchi, negando all'hacker i fondi rubati.

Ultima modifica: @omahs(opens in a new tab), 17 febbraio 2024

Questo tutorial è stato utile?