ERC-20 con barriere di sicurezza
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:
- Apri l'IDE di Remix(opens in a new tab).
- Clicca l'icona di clonazione di GitHub ().
- Clona la repository di GitHub
https://github.com/qbzzt/20220815-erc20-safety-rails
. - 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:
Seleziona ERC20.
Inserisci queste impostazioni:
Parametro Valore Nome SafetyRailsToken Symbol SAFE Premint 1000 Caratteristiche Nessuno Access Control Ownable Upgradability Nessuno 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.
Ora, abbiamo un contratto ERC-20 pienamente funzionante. Puoi espandere
.deps
>npm
per visualizzare il codice importato.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:
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.
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 virtual3 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 virtualCopia
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 adaddress(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:
Congelamento e scongelamento degli account. Utile, ad esempio, quando un account potrebbe essere compromesso.
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:
- I contratti
Ownable
(opens in a new tab) hanno un singolo proprietario. Le funzioni aventi il modificatore(opens in a new tab)onlyOwner
possono essere chiamate soltanto dal proprietario. I proprietari possono trasferire la proprietà a qualcun altro o rinunciarvi completamente. I diritti di tutti gli altri account sono solitamente identici. - I contratti
AccessControl
(opens in a new tab) hanno il controllo d'accesso basato sul ruolo (RBAC)(opens in a new tab).
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;CopiaEventi(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 unfrozen2 event AccountFrozen(address indexed _addr);3 event AccountThawed(address indexed _addr);CopiaFunzioni 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 public3 onlyOwnerCopiaLe 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 } // freezeAccountCopiaSe 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 dest4 )5 public6 onlyOwner7 {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