Contratti metamorfici: cosa sono? come funzionano? sono sicuri? Quando usarli? Ecco le risposte.

(Articolo scritto in collaborazione tra la redazione SCT Italia ed il team di Radix Srl. In particolare ringraziamo Francesco Cabras e Giovanni Casu per il loro supporto)


I contratti metamorfici rappresentano un concetto rivoluzionario nell'ambito degli smart contract, introducendo adattabilità e flessibilità dinamiche. A differenza dei contratti tradizionali con termini fissi, i contratti metamorfici possono modificare autonomamente il loro codice e comportamento in base a trigger o condizioni predefinite. Questa caratteristica consente loro di adattarsi ed evolversi in risposta a circostanze mutevoli, rendendoli estremamente versatili e adattabili.

I contratti metamorfici offrono un'adattabilità e una flessibilità senza pari, rivoluzionando il concetto di smart contract. Consentono l'applicazione versatile in diverse industrie. Ad esempio, nei servizi basati su abbonamento, il contratto può regolare automaticamente prezzi, termini o livelli di servizio in base al comportamento dell'utente o alle condizioni di mercato. Nella gestione della supply chain, il contratto può adattarsi ai cambiamenti nella logistica, negli accordi di prezzo o nelle normative. Le applicazioni DeFi possono beneficiare dei contratti auto-modificanti aggiornando i tassi di interesse, i requisiti di collaterale o altri parametri in risposta alle fluttuazioni di mercato.

Concetti chiave e i design pattern dei contratti metamorfici

I concetti chiave e i design pattern sono cruciali per l'implementazione di successo dei contratti metamorfici. I condizionali e i trigger all'interno del codice del contratto definiscono le circostanze in cui il contratto modifica il suo comportamento o aggiorna i suoi termini. Separare la logica principale dai moduli aggiornabili migliora la sicurezza e la manutenibilità, consentendo aggiornamenti fluidi e garantendo la retrocompatibilità. L'upgradabilità degli smart contract e la modularità giocano un ruolo fondamentale nell'efficacia dei contratti metamorfici. L'upgradabilità consente ai proprietari di contratti di introdurre modifiche o miglioramenti senza la necessità di rilasciare nuovamente il contratto, riducendo al minimo le interruzioni delle transazioni in corso. Il design modulare consente l'upgradabilità indipendente di specifiche funzionalità del contratto, offrendo un controllo granulare sulle caratteristiche adattive.

Come creare contratti metamorfici

Ci sono vari modi per creare contratti metamorfici, ma un approccio relativamente semplice prevede i seguenti passaggi:

  1. Inizialmente, deploy di un contratto di implementazione che non si basi su un costruttore, ma che possa avere una funzione regolare (come ad esempio initialize) che svolge un ruolo simile. Questo contratto di implementazione dovrebbe anche avere la capacità di auto-distruggersi.

  2. Memorizza un riferimento all'indirizzo del contratto di implementazione nello storage in una posizione fissa e nota. Il contratto factory, che inizia la chiamata CREATE2, è una scelta adeguata per questo scopo.

  3. Utilizza CREATE2 per deployare un contratto metamorfico con un codice di inizializzazione fisso e non deterministico. Questo codice recupera l'indirizzo di implementazione dalla funzione del factory, clona il bytecode di runtime in quella posizione e deploya il bytecode di runtime per il contratto metamorfico. In alternativa, puoi utilizzare un contratto intermedio transitorio con un codice di inizializzazione fisso che deploya il contratto metamorfico utilizzando CREATE e quindi si auto-distrugge immediatamente.

  4. Quando è necessario modificare il contratto metamorfico, semplicemente self-destruct il contratto esistente, deploya e fa riferimento a una nuova implementazione e redeploya il contratto. Poiché il codice di inizializzazione rimane lo stesso, l'indirizzo del contratto metamorfico rimarrà invariato.

Modificare il bytecode del contratto utilizzando CREATE2, CREATE e SELFDESTRUCT

In Ethereum, è possibile modificare il bytecode di un account fisso utilizzando una sequenza di chiamate CREATE/CREATE2 e SELFDESTRUCT. Queste operazioni comportano il deploy e la distruzione dei contratti in un ordine specifico. Comprendere queste operazioni è essenziale per comprendere i concetti discussi in questo articolo. Mentre la documentazione di Ethereum fornisce spiegazioni dettagliate, ecco una panoramica breve:

CREATE/CREATE2 sono operazioni uniche della Ethereum Virtual Machine (EVM) utilizzate per creare nuovi account. Un contratto può essere creato solo se l'account di destinazione viene considerato "vuoto", ovvero ha una dimensione del codice pari a 0 e un nonce pari a 0. Quando un contratto viene creato, la sua dimensione del codice diventa maggiore di 0 e il nonce diventa 1. Sebbene queste informazioni siano comunemente note, vale la pena sottolineare alcuni dettagli chiave su CREATE e CREATE2:

CREATE: Calcola l'indirizzo del nuovo contratto utilizzando la formula keccak256(deployer_addr, deployer_nonce) (specificamente, keccak256(rlp([deployer_addr, deployer_nonce]))[12:]).

CREATE2: Calcola l'indirizzo del nuovo contratto utilizzando la formula keccak256(_0xFF, deployer_addr, salt, bytecode) (specificamente, keccak256(_0xFF, deployer_addr, salt, bytecode)[12:]).

In contrasto, SELFDESTRUCT "cancella" un account ripristinandone il bytecode e il nonce. Ai fini di questo articolo, è fondamentale notare che SELFDESTRUCT reimposta il nonce dell'account, consentendo l'uso di CREATE più volte dallo stesso indirizzo con lo stesso nonce.

Il procedimento

Utilizzando la sequenza CREATE -> SELFDESTRUCT -> CREATE -> SELFDESTRUCT -> ... con lo stesso bytecode dallo stesso indirizzo, ogni nuova operazione CREATE deploya un contratto in un nuovo indirizzo perché il "deployer_nonce" aumenta ad ogni transazione.

D'altra parte, se utilizziamo la sequenza CREATE2 -> SELFDESTRUCT -> CREATE2 -> SELFDESTRUCT -> ... con lo stesso bytecode e salt, l'indirizzo risultante rimarrà lo stesso ad ogni iterazione.

Tuttavia, è importante notare che nel primo caso (con CREATE), l'indirizzo di deploy può essere lo stesso se il "deployer_nonce" rimane invariato. Ecco il trucco: utilizzando SELFDESTRUCT, possiamo reimpostare il nonce dell'indirizzo in cui risiede il contratto creato con CREATE2, consentendone il redeploy. Successivamente, utilizzando CREATE, possiamo deployare un nuovo contratto dallo stesso indirizzo ma con un codice diverso.

Combinate queste operazioni, possiamo implementare il seguente scenario di esempio:

  1. Deployare il contratto MutDeployer utilizzando CREATE2. Il nuovo contratto avrà un nonce di 1 (secondo EIP161).

  2. MutDeployer deploya MutableV1 (la prima versione del codice mutabile) utilizzando CREATE. Il nuovo contratto viene deployato all'indirizzo keccak256(MutDeployerAddr, MutDeployerNonce == 1).

  3. Gli utenti interagiscono con MutableV1, assumendo che il suo codice rimanga costante.

  4. Il proprietario esegue SELFDESTRUCT su MutableV1 per successivamente deployare MutableV2 allo stesso indirizzo.

  5. Il proprietario esegue SELFDESTRUCT su MutDeployer, rendendo il suo account vuoto (codesize == 0 e nonce == 0).

  6. L'utente ripete il passo 1, deployando lo stesso contratto MutDeployer utilizzando CREATE2 allo stesso indirizzo. Ora, MutDeployer ha un nonce di 1, proprio come nel passo 1.

  7. MutDeployer deploya MutableV2 con un nuovo bytecode utilizzando CREATE (simile al passo 2). Il nuovo contratto viene deployato allo stesso indirizzo, keccak256(MutDeployerAddr, MutDeployerNonce == 1).

  8. Gli utenti continuano ad interagire con MutableV2 utilizzando lo stesso indirizzo di MutableV1.

Si noti che questo processo consente la modifica del codice del contratto mantenendo lo stesso indirizzo, consentendo aggiornamenti fluidi dei contratti senza richiedere agli utenti di cambiare l'indirizzo con cui interagiscono. Ecco di seguito il codice che realizza i vari passaggi.

Potenziali rischi a modifiche dinamiche del codice

Tuttavia, la modifica dinamica del codice comporta anche potenziali rischi e vulnerabilità. Garantire la sicurezza e l'integrità delle capacità adattive diventa cruciale, richiedendo robuste misure di sicurezza e test approfonditi per prevenire modifiche non autorizzate o sfruttamenti. La complessità dei contratti metamorfici solleva preoccupazioni legali e normative riguardanti l'efficacia e gli obblighi contrattuali. La collaborazione con gli enti di regolamentazione e un'attenta analisi legale sono indispensabili per garantire la conformità. Trovare un equilibrio tra autonomia e governance è fondamentale per evitare conseguenze indesiderate o abusi delle funzionalità adattive, rendendo necessari chiari quadri di governance e meccanismi di supervisione.

Come difendersi dai contratti metamorfici maligni

Per difendersi dai contratti metamorfici maligni, sono necessari controlli approfonditi e meccanismi di rilevamento. Il processo di rilevamento è ampiamente spiegato nel detector e nell'articolo menzionati, che consentono di verificare se un indirizzo può avere un codice mutabile. Ecco un riassunto dei controlli chiave:

  1. Assicurarsi che il contratto non contenga l'opcode SELFDESTRUCT o utilizzi DELEGATECALL verso un contratto con SELFDESTRUCT.

  2. Verificare se CREATE2 è stato utilizzato per il deploy, tra gli altri indicatori.

  3. Assicurarsi che il contratto sia stato inizialmente deployato da una fonte che non consente il redeploy. Questo può essere realizzato evitando l'uso di CREATE2 o tenendo traccia di ogni deploy e impedendo duplicazioni. Inoltre, è necessario verificare che il deployer stesso non sia in grado di metamorfosi.

  4. Prima di procedere con la transazione, verificare che il contratto con cui si interagisce non sia cambiato utilizzando metodi come EXTCODEHASH o meccanismi simili all'inizio della transazione.

Per la maggior parte delle applicazioni legittime di CREATE2, come negli “state channel” e nell'istanziazione controfattuale, queste precauzioni non dovrebbero rappresentare sfide significative. In generale, fare attenzione quando si interagisce con qualsiasi contratto che può auto-distruggersi o subire modifiche pericolose. Tuttavia, se si cerca un contratto leggero con capacità di aggiornamento appropriate e controlli adeguati, non è necessario cercare altrove.

Effettuando questi controlli, è possibile accertare che il codice del contratto sia immutabile. Tuttavia, è importante notare che in scenari di sicurezza esiste sempre la possibilità di eludere la rilevazione costruendo scenari complessi utilizzando catene di DELEGATECALL o altre tecniche. Pertanto, bisogna fare attenzione.

Per riassumere il confronto tra i contratti metamorfici e i transparent proxies, possiamo evidenziare i seguenti punti:

Persistenza dello storage: I transparent proxies preservano lo storage durante gli aggiornamenti, mentre i contratti metamorfici azzerano completamente lo stato, inclusi i saldi degli account. I transparent proxies sono comunemente utilizzati per contratti ERC20 o ERC721 aggiornabili, mentre i contratti metamorfici possono essere più adatti per contratti di identità ERC725 o altri contratti di auto-sovranità.

Overhead delle chiamate di contratto: Chiamare un contratto metamorfico comporta meno overhead rispetto alla chiamata a un transparent proxy. I transparent proxies devono verificare il chiamante e delegare quindi la chiamata a un contratto di logica.

Processo di aggiornamento: Gli aggiornamenti dei contratti metamorfici sono meno fluidi perché le operazioni di selfdestruct vengono registrate nella sotto-transazione di una transazione ed eseguite alla fine della transazione. Ciò significa che un aggiornamento richiede due transazioni, risultando in un codice di contratto temporaneamente vuoto (che può essere suscettibile a utilizzi intermedi) tra le transazioni. D'altra parte, se un transparent proxy incontra un selfdestruct, verrà completamente distrutto, mentre un contratto metamorfico può comunque essere recuperato.

Utilizzo del costruttore: Nessuno dei due metodi permette l'utilizzo del costruttore durante l'inizializzazione del contratto. Invece, di solito viene utilizzata una funzione di inizializzazione immediatamente dopo aver impostato il nuovo contratto con l'implementazione clonata. L'eccezione a questa regola si verifica quando si utilizza un contratto transitorio intermedio deployato tramite CREATE2 per deployare il contratto metamorfico tramite CREATE. In tali casi, i costruttori possono comunque essere utilizzati durante il deploy del contratto metamorfico.

Si tenga presente che queste sono considerazioni generali e l'adeguatezza di ciascun approccio dipende dai requisiti specifici e dai casi d'uso del contratto in fase di sviluppo.

Caso studio: l'attacco ai contratti di governance di Tornado Cash.

Un individuo con intenzioni malevole ha presentato con successo una proposta ingannevole che è passata inosservata ed è stata successivamente approvata dai votanti del token DAO. Di conseguenza, è riuscito ad ottenere 1,2 milioni di voti e a prendere il controllo della situazione.

Attraverso una serie di eventi imprevisti, la governance di Tornado Cash è stata dirottata mediante una proposta ingannevole, simile a un cavallo di Troia. Di conseguenza, questa proposta ha conferito effettivamente il completo controllo del DAO a un singolo indirizzo.

Nonostante gli smart contract abbiano impedito il prelievo di circa 275 milioni di dollari dai pool di privacy, l'autore dell'attacco è riuscito ad ottenere il controllo del token di governance TORN. Ciò gli ha permesso di modificare il router e di dirottare depositi e prelievi, nonché di ottenere privilegi amministrativi su Nova, il deployment sulla Gnosis chain.

Nel corso dell'anno, il crypto mixer di riferimento per DeFi ha affrontato una serie di difficoltà. Queste includono l'imposizione di sanzioni OFAC nell'agosto e l'incarcerazione del core developer Alexey Pertsev, che è stato successivamente rilasciato in attesa di processo.

Inoltre, alcune settimane fa sono sorte preoccupazioni riguardo a un potenziale sfruttamento del sistema di governance di Tornado. Questo coinvolgeva la creazione di indirizzi multipli e il blocco di 0 token TORN nella vault di governance. Tuttavia, poiché non sono state osservate conseguenze immediate, ciò è stato alla fine ignorato come un tentativo fallito.

Inganno oppure mossa preparatoria?

Gli indirizzi degli autori dell'attacco sono i seguenti:

Indirizzo dell'autore dell'attacco 1: 0x092123663804f8801b9b086b03b98d706f77bd59

Indirizzo dell'autore dell'attacco 2: 0x592340957ebc9e4afb0e9af221d06fdddf789de9

Il contratto della proposta: https://etherscan.io/address/0xC503893b3e3c0C6b909222b45f2a3a259a52752D

L'attacco è stato eseguito in modo discreto, nascosto all'interno di una proposta mirata a penalizzare specifici relayers sospettati di comportamenti disonesti. Sebbene il codice sembrasse utilizzare una logica simile a una proposta precedente, l'autore dell'attacco aveva incorporato una funzione aggiuntiva che gli consentiva di attivare l'autodistruzione del contratto.

Il contratto della proposta è stato rilasciato tramite un contratto deployer, utilizzando una combinazione di opcode CREATE e CREATE2. Sfruttando il processo di deploy deterministico, l'attaccante ha sfruttato la possibilità di deployare nuovo codice all'indirizzo autorizzato dalla governance.


Sfruttando la funzione selfDestruct, l'hacker è riuscito con successo a eliminare il codice autorizzato, reimpostando così il proprio nonce nel processo. Questa manipolazione gli ha consentito di rilanciare il contratto malevolo allo stesso identico indirizzo.

Dettagli:

Opzione A: Utilizzare l'opcode CREATE2 per creare un contratto di proposta maligno. Questo solleverà un allarme poiché CREATE2 e self-destruct sono utilizzati insieme.

Opzione B: Utilizzare CREATE2 per creare il contratto deployer (0x7dc8), che a sua volta deploya il contratto di proposta maligno (0xc503) utilizzando CREATE.

Successivamente, il contratto deployer viene autodistrutto (per reimpostare il nonce) e può creare una nuova proposta allo stesso indirizzo (0xc503).

Il motivo di ciò risiede nel fatto che l'indirizzo del contratto deployato utilizzando CREATE è determinato dal mittente e dal valore del nonce (in questo caso, nonce = 1). Sfruttando ciò, l'attaccante è stato in grado di generare una nuova proposta con un indirizzo identico (0xc503).

Queste tecniche per creare "contratti metamorfici" sono una delle ragioni per cui alcuni hanno chiesto che l'opcode selfDestruct venga deprecato.

La proposta malevola ha quindi assegnato 10.000 TORN a tutti gli indirizzi creati nell'attacco presunto della scorsa settimana. Questi sono stati sbloccati e prelevati dalla vault, conferendo all'attaccante 1,2 milioni di voti (contro i 700.000 voti legittimi) e il pieno controllo della governance di Tornado.

Consultare il grafico di BlockSec per una panoramica completa delle varie fasi coinvolte nell'attacco:


In un'imprevista svolta degli eventi, l'attaccante ha successivamente presentato una proposta per annullare le conseguenze della sua presa di controllo ostile, restituendo effettivamente il controllo al DAO come prima dell'attacco.

Oltre al profitto di circa 430 ETH (~750.000 dollari) dalla vendita di TORN (qualche indizio su dove l'attaccante abbia scelto di riciclare i profitti?), l'hacker potrebbe avere altre motivazioni:

O si tratta di una grande trolling o si rivelerà una lezione costosa ma non disastrosa sulla sicurezza della governance.

Come ha sottolineato il membro della comunità Tornadosaurus-Hex:

Nota che non abbiamo nemmeno scelta riguardo a questa proposta, ma è comunque importante.

CONCLUSIONE (Importanza di test approfonditi e audit)

Test approfonditi e audit sono di importanza fondamentale per mitigare i rischi associati ai contratti metamorfici. Test rigorosi garantiscono l'affidabilità e la funzionalità delle capacità adattive del contratto. Audit completi aiutano ad individuare vulnerabilità e potenziali exploit, rafforzando complessivamente la sicurezza del contratto.

In SCT ITALIA stiamo costruendo una rete di esperti specializzati nella sicurezza degli smart contract, che attraverso analisi di sicurezza approfonditi, possono identificare vulnerabilità e debolezze negli smart contract, contribuendo a proteggere dai tentativi di sfruttamento maligno e garantendo l'integrità dei sistemi di governance.