La sicurezza degli smart contract
Estratto dello speech tenuto da Marco Michelino il 30/5/2023 per Offchain Milan.
Da anni gli appassionati di blockchain fantasticano sull’arrivo della mass adoption; ad oggi questa non si è ancora verificata per una serie di motivi tra cui un esperienza utente non soddisfacente.
Uno degli aspetti peggiori dell’esperienza utente nelle cripto è quello della sicurezza.
Il sito https://rekt.news riporta informazioni sui principali attacchi avvenuti ai danni degli smart contract negli ultimi tre anni; sono esclusi quindi gli hack ai danni dei wallet degli utenti, che sono sicuramente molto più numerosi.
In questo articolo cerchiamo di capire perché avvengano così tante compromissioni ai danni delle cripto e se esistano modi per prevenirle che siano alla portata di utenti non esperti.
Token nativi e non
Prima di entrare nel vivo dell’argomento ricordiamo cosa sia un token nativo di una blockchain e cosa sia un token non nativo.
BTC ed ETH sono token nativi delle rispettive blockchain, questo significa che i rispettivi network sanno cosa sia un BTC o un ETH e sanno come questi possano essere trasferiti da un wallet ad un altro.
Per movimentare degli ETH ho bisogno di firmare una transazione con la chiave privata del mio wallet, la rete Ethereum capisce direttamente la transazione e non esiste alcun modo alternativo attraverso il quale un ETH possa essere spostato da un wallet ad un altro.
Su Ethereum, però, è possibile creare token di tipo differente; andiamo ora ad analizzare un ERC-20 che è uno degli standard che posso sfruttare per creare token su Etherem.
Un ERC-20 è fondamentalmente una sorta di database che contiene informazioni quali la quantità di token posseduta da ogni wallet, più una serie di metodi (pezzi di codice) che possono essere richiamati da wallet o da altri smart contract.
La rete Ethereum non “capisce” gli ERC-20, non sa come questi possano essere movimentati, per questo il trasferimento avviene invocando uno dei metodi esposti dal token stesso, cioè del codice che è stato scritto dallo sviluppatore che lo ha creato.
Lo standard ERC-20 prevede che esista un metodo denominato transfer() che va utilizzato per movimentare un token verso un nuovo indirizzo.
Quali sono, quindi, le differenze dal punto di vista della sicurezza nell’utilizzo di un ERC-20 da quello di un ETH?
- Devo eseguire del codice scritto da qualcuno fidandomi che questo faccia davvero quello che gli ho chiesto di fare e non (per imperizia o per dolo) qualcosa di diverso.
Certo, potrei analizzare il codice ma questa non è una possibilità alla portata di chiunque. - Possono esistere (per imperizia, per dolo o per motivi funzionali al token stesso) altri modi in cui i miei token possono essere movimenti, magari anche da soggetti diversi da me.
Anzi, almeno un metodo del genere esiste sicuramente, si chiama transferFrom(), e serve proprio per movimentare token altrui (!!!).
Lo standard prevede che io autorizzi preventivamente terzi a spostare i miei token attraverso il metodo approve(); anche qui non è affatto scontato che le cose vadano sempre secondo i piani.
Come funziona una dApp?
Analizziamo il funzionamento di Uniswap per capire come operino, in generale, molti smart contract su Ethereum.
Da un punto di vista concettuale il funzionamento di Uniswap è semplice: mi permette di scambiare una certa quantità di un ERC-20 (che denominiamo A) per una certa quantità di un altro ERC-20 (che denominiamo B).
Mi aspetterei quindi che io debba inviare A ad Uniswap ed in cambio riceverò B; purtroppo questo approccio non sarebbe affidabile: l’operazione di trasferimento di A verso Uniswap e quella tramite la quale Uniswap invia B a me sarebbero due transazioni distinte; se per qualche motivo una delle due dovesse fallire mentre l’altra va a buon fine avremmo un problema.
Per questo su Ethereum è necessario che sia lo stesso soggetto a movimentare sia i token A da me ad Uniswap sia i token B in direzione opposta.
Quindi ciò che avviene in realtà è che io devo chiamare il metodo approve() per autorizzare Uniswap a prendere i miei token A e poi chiamare il metodo trade() all’interno del quale Uniswap esegue (dopo una serie di verifiche) transferFrom() su A e transfer() su B.
In questo modo i due trasferimenti avvengono in un’unica transazione.
Cosa mai potrebbe andare storto?
Ci sono almeno due criticità nello schema illustrato:
- Sono costretto, tramite il metodo approve(), ad affidare ad un soggetto terzo la possibilità di fare ciò che desidera con i miei token A.
Sono di nuovo nella situazione di dover analizzare il funzionamento di uno o più smart contract oppure di fidarmi di essi.
Peggio ancora: se in futuro qualcuno riuscisse a compromettere Uniswap, potrebbe rubare anche i token A che si trovano all’interno del mio wallet, non devo quindi solo preoccuparmi che Uniswap non sia malevolo ma anche che sia sicuro da eventuali attacchi futuri da parte di terzi. - Uniswap deve servirsi di metodi esterni per trasferire i token A e B.
Abbiamo già discusso del rischio con riferimento al mio wallet ma qui la situazione è ancora più delicata: i due token A e B potrebbero essere stati sviluppati dopo Uniswap; lo sviluppatore di Uniswap potrebbe non avere quindi alcuna possibilità di verificare che questi non siano malevoli, al momento della scrittura del codice si trovava a dover fronteggiare un avversario del tutto sconosciuto.
Esistono diversi espedienti attraverso i quali i token A e B potrebbero attaccare uno smart contract che esegua un loro metodo, il più comune è la reentrance.
Reentrance
La reentrance è la possibilità, da parte di uno smart contract chiamato, di richiamare a sua volta un metodo dello smart contract chiamante.
Se non gestita correttamente, questa possibilità può sovvertire la logica di funzionamento dello smart contract chiamante.
Immaginiamo uno smart contract che consenta ad un utente di depositarvi dei token A e successivamente di ritirarli tramite un metodo withdraw() che accetti come argomento il nome del token da ritirare e la relativa quantità.
All’atto del deposito verrà valorizzata una opportuna variabile interna dello smart contract (chiamiamola depositati) con la quantità di token depositati dallo specifico wallet.
Una ipotetica implementazione del metodo withdraw( A, n) potrebbe essere:
verifica che n sia minore o uguale a depositati[A, wallet_chiamante], altrimenti fallisci
A.transfer(wallet_chiamante, n)
aggiorna depositati[A, wallet_chiamante] = depositati[A, wallet_chiamante] - n
E’ così semplice che sembra impossibile sbagliare eppure questo pseudocodice è vulnerabile ad un attacco di tipo reentrance.
Supponiamo che il metodo transfer() del token A chiami a sua volta withdraw( A, n), supponiamo inoltre che wallet_chiamante abbia in precedenza depositato 100 token A nel nostro smart contract, il flusso di esecuzione di withdraw( A, 100 ) sarebbe questo:
verifica che 100 sia minore o uguale a 100, altrimenti fallisci
A.transfer(wallet_chiamante, 100)
verifica che 100 sia minore o uguale a 100, altrimenti fallisci
A.transfer(wallet_chiamante, 100)
aggiorna depositati[A, wallet_chiamante] = 100 - 100
aggiorna depositati[A, wallet_chiamante] = 0 - 100
Avevamo depositato 100 token A ma ne abbiamo ritirati 200!
La terza, quarta e quinta riga rappresentano l’attacco reentrance di A.transfer() ai danni di withdraw().
Alla fine il valore immagazzinato nella variabile depositati[A, wallet_chiamante] sarà -100, il problema è che andiamo ad aggiornare questa variabile troppo tardi, quando ormai la frittata è fatta.
Anzi, se A. transfer() continuasse a chiamare withdraw() ogni volta, andremmo ad avere chiamate multiple innestate fino a prosciugare tutti i token A depositati da tutti gli utenti nel nostro smart contract.
Da notare che invertendo la seconda e la terza riga del nostro pseudocodice il metodo non sarebbe più vulnerabile, il flusso di esecuzione diventerebbe questo:
verifica che 100 sia minore o uguale a 100, altrimenti fallisci
aggiorna depositati[A, wallet_chiamante] = 100 - 100
A.transfer(wallet_chiamante, 100)
verifica che 100 sia minore o uguale a 0, altrimenti fallisci
La quarta riga fa sì che l’esecuzione del codice si interrompa bruscamente.
E’ responsabilità dello sviluppatore capire le possibili conseguenze sul codice da lui scritto a fronte di comportamenti strani ed inaspettati di codice scritto da altri!
Se vi è sembrato non ovvio l’attacco a tre sole righe di pseudocodice immaginate cosa possa significare tutto questo per uno smart contract mediamente complesso…
Possibili contromisure
Chi conosce il mondo Ethereum probabilmente fino a questo punto avrà storto un po’ il naso perché sa che esistono possibili contromisure alle criticità evidenziate; andiamo a vedere quali siano queste contromisure e valutiamo se siano davvero efficaci, soprattutto nell’ottica di un neofita.
Revoke
Il sito https://revoke.cash/ mi consente di verificare tutti gli smart contract cui ho in passato garantito approve() e di revocare loro la possibilità di spendere i miei token.
A mio avviso esistono tre motivi per cui questo non rappresenta una soluzione davvero efficace:
- Le interfacce degli smart contract mi guidano nell’approve() ma non nel revoke, l’utente deve sapere che questo sito esiste e deve ricordarsi di utilizzarlo.
In generale, uno strumento di sicurezza, specialmente se destinato ad utenti non smaliziati, dovrebbe avere dei default stringenti. - Anche l’utente che sa dell’esistenza di Revoke potrebbe essere restio nell’utilizzarlo dal momento che gli costa in termini di tempo e denaro.
Eseguire Revoke infatti richiede la spesa di alcune fee e mi costringe a rieseguire approve() (che anche ha un costo) la prossima volta che utilizzerò lo stesso smart contract.
E’ molto più pratico ed economico non usare Revoke per gli smart contract che utilizzo frequentemente. - Anche nel caso in cui io utilizzi Revoke, questo non fa altro che restringere la finestra temporale durante la quale sono attaccabile: non elimina la vulnerabilità ma si limita a trasformarla in una race condition.
Le persone oggi usano bot per qualsiasi cosa, niente di più facile che attaccarmi appena faccio approve() senza darmi il tempo di utilizzare Revoke.
Gli audit e la prova del tempo
Esistono aziende specializzate nella verifica della sicurezza degli smart contract (audit), posso pagare una di queste aziende per ottenere un certificato con cui fregiarmi presso i miei possibili utenti.
Sfortunatamente, anche se gli smart contract sottoposti ad audit sono generalmente più sicuri di quelli che non lo sono stati, il già citato rekt.news è ricco di compromissioni di smart contract che erano stati giudicati sicuri.
Questo accade anche perché un audit ha un costo non indifferente ed andrebbe rieseguito ad ogni modifica dello smart contract, cosa che non sempre lo sviluppatore ha voglia/risorse di fare.
Aggiungiamo che il povero utente finale difficilmente può riuscire a capire se l’audit si riferisce alla versione corrente dello smart contract o ad una precedente.
Anche l’anzianità e la popolarità di uno smart contract vengono spesso considerate indicazioni della sua sicurezza.
Pure questa credenza è valida fino ad un certo punto: esistono, purtroppo, smart contract che sono stati attaccati con successo anni dopo la loro pubblicazione.
Esistono anche smart contract che sono stati bucati, la cui falla è stata riparata, sono stati bucati di nuovo e ri-corretti ripetutamente.
Ma perché non si riesce a stabilire con certezza se uno smart contract è sicuro e a sistemarlo definitivamente?
Il problema nasce dalla indecidibilità del software: dato un pezzo di codice sufficientemente complesso è impossibile stabilire se tutti i possibili cammini di esecuzione siano corretti o se determinate condizioni possano causare comportamenti indesiderati.
In breve bisognerebbe testare una ad una tutte le possibili situazioni che potrebbero verificarsi allo scopo di sapere se una di queste sia problematica.
In altri termini: non puoi sapere se un software ha difetti o meno finché non ne trovi uno.
Simulazione delle transazioni e intent based transactions
Blind signing (firmare alla cieca) è il temine comunemente utilizzato per indicare il fatto che un wallet non è in grado di mostrare in forma intellegibile all’utente che effetti avrà la transazione che sta andando a firmare.
Non è solo un problema di interfaccia: il wallet di solito non ha modo di sapere che cosa comporti chiamare un certo metodo di un certo smart contract.
Alcuni wallet cercano di superare questo problema eseguendo al proprio interno una simulazione della transazione stessa; esiste cioè una EVM (Ethereum Virtual Machine) all’interno del wallet che va effettivamente ad eseguire lo smart contract e mostra il risultato ottenuto nell’ambiente simulato.
Anche questo espediente, per quanto possa sembrare risolutivo, ha grosse pecche.
Non tutte le transazioni hanno infatti un effetto immediatamente visibile: basti pensare al metodo approve(), non sembra produrre alcun effetto ma può essere molto dannoso se sto autorizzando uno smart contract malevolo o buggato a movimentare i miei token.
Inoltre nulla mi garantisce che il risultato della simulazione sia identico a quello che accadrà quando effettuerò davvero la transazione.
Possono cambiare condizioni al contorno (ad esempio l’output restituito da un oracolo) ma può cambiare anche il comportamento dello smart contract stesso.
L’indecidibilità ancora una volta ci rompe le uova nel paniere: non esiste modo semplice per stabilire se uno smart contract può avere comportamenti differenti o meno.
Un esempio banalissimo: immaginiamo uno smart contract che si comporti in modo onesto il 50% delle volte ed in modo malevolo il restante 50% delle volte in modo del tutto casuale. Ebbene, in questa situazione, esiste il 25% di probabilità che la simulazione mi indichi la transazione come sicura mentre quando vado ed eseguirla vengo truffato.
In questi giorni si parla molto di intent based transaction: un linguaggio comprensibile all’uomo in cui si possa esprimere cosa si desideri fare e quale risultato ci si attenda.
Il problema di questo approccio è che la rete Ethereum non è in grado di elaborare transazioni espresse in questo linguaggio; ci sarà un soggetto da qualche parte che tradurrà la intent based transaction in una transazione Ethereum.
Qui ricadiamo nel problema precedente: dal momento che la transazione non è sempre prevedibile non abbiamo modo di sapere se l’intenzione espressa dall’utente sarà davvero soddisfatta.
La reazione delle community ad una compromissione
Troppo spesso le comunità cripto sono iperprotettive nei confronti dei progetti che amano, così come un tifoso stravede per la propria squadra del cuore.
Il fatto che esistano diversi problemi noti e relative soluzioni tampone ad essi (anche se non del tutto soddisfacenti) ci fa spesso addossare la colpa all’imperizia dell’utente o allo sviluppatore.
In altri termini diamo per scontato che chiunque si avvicini alle cripto debba avere un ampio bagaglio di competenze specifiche.
Questa mentalità, però, va nella direzione opposta della mass adoption: stiamo creando un gap tra coloro che possono usare le cripto e coloro che non possono.
Pensa allo stato d’animo dell’utente che ha subito un furto e si vede addossare la colpa alla propria imperizia.
Al danno si aggiunge la beffa, non può esistere una esperienza utente peggiore!
Tutte le tecnologie di ampio successo tendono invece ad essere usabili da un utente neofita: non hai bisogno di sapere a cosa serva esattamente una sonda lambda per guidare un’automobile così come puoi effettuare una telefonata anche se non hai mai neanche sentito nominare lo spread spectrum e il beamforming.
Piattaforme non EVM
Fino qui ho sempre citato esplicitamente la piattaforma Ethereum; esistono altre reti come BSC e Avalanche che hanno scelto la strada della compatibilità con Ethereum, implementando la EVM nei propri nodi.
Ovviamente tutte le considerazioni fin qui effettuate si applicano allo stesso modo a queste reti.
Esistono però anche piattaforme che hanno sviluppato una propria tecnologia di smart contract, magari prendendo spunto da EVM e risolvendo qualche problema noto.
E’ il caso, ad esempio, di Solana, Aptos, Near, MultiversX…
La maggior parte di queste piattaforme ha risolto in modo sistematico la reentrance, impedendola a livello di piattaforma.
Sfortunatamente la reentrance è solo uno dei modi in cui è possibile attaccare uno smart contract.
Le due criticità che ho evidenziato all’inizio (la necessità da parte dell’utente di delegare la movimentazione dei propri token e la necessità da parte dello smart contract di usare codice esterno ignoto per trasferire i token) restano fondamentalmente invariate.
A titolo di esempio: è molto comune su Solana che mettendo fondi in un wallet precedentemente utilizzato per acquistare un NFT questi spariscano; è accaduto non solo a degli sprovveduti ma anche ad un noto influencer che ha riutilizzato un wallet utilizzato sei mesi prima per l’acquisto di un NFT.
Sembra che l’equivalente Solana di approve() sia ancora peggio di quello di Ethereum: consente anche di rubare anche SOL!
Turing completeness
Alcune piattaforme, nel tentativo di eliminare l’indecidibilità, hanno adottato linguaggi di programmazione non Turing complete.
Un linguaggio non Turing complete difetta di alcuni costrutti che causano l’indecidibilità per cui è possibile dimostrare la correttezza o meno di un software scritto in uno di questi linguaggi; in altri termini, i linguaggi non Turing complete sono deterministici.
Il prezzo da pagare è che alcuni algoritmi potrebbero essere più complessi da implementare o addirittura impossibili.
Sfortunatamente se il codice deve richiamare codice sconosciuto scritto da terzi, beh, il tutto ritorna nuovamente imprevedibile.
Diciamo comunque che un linguaggio non Turing complete associato alla impossibilità di fare reentrance rappresenta sicuramente un grosso passo in avanti in termini di sicurezza.
Un esempio di linguaggio di programmazione non Turing complete è Move, ideato da Meta (Facebook), adottato da Aptos e Sui.
Purtroppo queste contromisure si sono dimostrate insufficienti se poco tempo dopo il lancio gli sviluppatori di Sui si sono sentiti in dovere di mettere in guardia gli utenti.
Ancora una volta è l’utente che deve prestare attenzione per far fronte alle mancanze della piattaforma.
Piattaforme con token nativi
Immaginiamo ora una piattaforma in cui tutti i token siano nativi, in cui non sia necessario utilizzare codice scritto da terzi per trasferire un token perché la rete se come movimentare quel token. Vediamone alcune:
Nel gennaio 2014 nacque la blockchain NXT che fece tanto scalpore ed arrivò subito in seconda posizione su CoinMarketCap.
Una delle caratteristiche di questa piattaforma è quella di poter creare propri token semplicemente dal wallet, senza dover scrivere una riga di codice.
La rete sa sempre come manipolare i nuovi token creati, che sono vere coin native così come gli NXT stessi.
Sull’onda del successo di NXT, fu creata NEM che nell’idea del fondatore doveva essere un clone di NXT con una migliore distribuzione; presto però il fondatore fu cacciato e gli sviluppatori decisero di spingersi oltre.
Su NEM è possibile creare una coin tramite il wallet, sempre senza dover scrivere codice, ma potendogli associare una serie di caratteristiche per adattarle a casi d’uso differenti (trasferibile/non trasferibile, aggiunta di royalties…).
Io sono personalmente molto legato a questo progetto, anche perché nel 2014, pochi mesi prima del lancio della mainnet, scoprii una vulnerabilità di tipo replay attack nella testnet e ricevetti una generosa donazione dagli sviluppatori.
Entrambi i progetti, dopo un successo iniziale, hanno visto ridurre la loro capitalizzazione soprattutto a seguito del lancio di Ethereum e dell’affermazione della narrativa degli smart contract.
Anche la blockchain Cardano consente all’utente di creare token nativi, senza scrivere codice ma, a diffenrenza delle precedenti, consente anche di scrivere smart contract per realizzare dApp.
Il linguaggio degli smart contract di Cardano è Haskell, un linguaggio non Turing complete.
Queste caratteristiche rendono Cardano, ad oggi, la piattaforma più sicura per lo sviluppo di smart contract: questi infatti non sono costretti ad eseguire codice esterno per movimentare token.
Cardano è inoltre l’unica piattaforma, fin qui analizzata, i cui smart contract siano completamente deterministici.
Purtroppo, varie problematiche non strettamente inerenti la sicurezza tra cui una difficile esperienza sviluppatore, hanno limitato finora il successo di questa piattaforma.
Radix rappresenta il prossimo passo nell’evoluzione di queste piattaforme: è un network in cui tutti i componenti dell’architettura sono stati pensati, non seguendo il mainstream tracciato da Ethereum, ma nell’ottica di realizzare la migliore piattaforma possibile per la finanza decentralizzata.
In questo articolo mi limiterò a parlare di smart contract e transazioni, tralasciando gli strati sottostanti come il consenso.
Il lancio degli smart contract avverrà tra luglio ed agosto 2023, in concomitanza con l’aggiornamento denominato Babylon.
Scrypto
Scrypto è il nome del linguaggio di programmazione degli smart contract su Radix, è basato su Rust che è un linguaggio noto per la sua robustezza e e le sue performance.
Rust è Turing complete ma, grazie ad una serie di concetti unici come l’ownership delle variabili, il codice Rust è quasi completamente deterministico.
Grazie al buon livello di determinismo, il compilatore Rust riesce, già in fase di compilazione, ad individuare e segnalare come errori molte anomalie che in altri linguaggi verrebbero scoperte solo al momento dell’esecuzione del codice (run-time).
Il compilatore, inoltre, obbliga il programmatore a gestire ogni possibile situazione eccezionale possa verificarsi nel corso dell’esecuzione del codice.
In pratica, nonostante la Turing completeness, non si verificano situazioni anomale non gestite nell’esecuzione di codice Rust.
Si sa che i programmatori commettono errori mentre sviluppano codice, tanti errori; potremmo che Rust rende molto più difficile commettere questi errori migliorando la developer experience.
Una developer experience migliore si tradurrà in maggiore produttività e, probabilmente, anche in una migliore user experence.
Ciò che differenzia Scrypto da Rust è l’essere asset oriented, cioè mentre gli altri linguaggi possono manipolare solo numeri e stringhe di caratteri, in Scrypto puoi manipolare token in modo naturale.
E’ possibile, ad esempio, passare un token come argomento di un metodo dentro un apposito contenitore chiamato bucket.
Anche il run-time di Scrypto è stato esteso per verificare situazioni in cui uno smart contract non sta gestendo correttamente gli asset ricevuti; ad esempio se un metodo non preleva tutti i token presenti nel bucket ricevuto, il run-time fa fallire la transazione.
Ancora, passare un token ad un metodo è l’unico modo possibile per depositarlo in uno smart contract: il wallet interno dello smart contract (chiamato vault) non è indirizzabile dall’esterno dello smart contract stesso.
Quindi lo smart contract ha facoltà di analizzare (ed eventualmente far fallire) anche le transazioni entranti; questo consente di eliminare numerose tipologie di attacchi di cui finora non abbiamo finora parlato che consistono proprio nel regalare token inattesi ad uno smart contract.
Tornando all’esempio iniziale sul funzionamento di Uniswap: con Scrypto puoi davvero svilupparlo seguendo la semplice logica: “tu invii il token A allo smart contract e lo smart contract t**i invia il token B in cambio”.
Niente approve(), niente trasfer() e niente rischi ad essi connessi!
Anziché costruire un colabrodo e poi affannarci a tappare i cento buchi che ha, abbiamo semplicemente costruito un contenitore privo di buchi.
Transaction manifest
Quando abbiamo parlato di simulazione delle transazioni e di intent based transactions, abbiamo visto che queste non potranno mai essere 100% affidabili in quanto gli smart contract non sono deterministici.
Una transazione Radix può contenere non solo trasferimento di token e chiamate a metodi di smart contract: è possibile inserirvi anche assert , ossia la definizione di condizioni che desideriamo vengano verificate durante o al termine dell’esecuzione della transazione stessa.
Tornando all’esempio di Uniswap, il mio wallet potrebbe generare una transazione del tipo "invi o un token A all’exchange decentralizzato e ricev o almeno un token B in cambio ".
Ho stabilito dentro la transazione che mi aspetto di ricevere il token B; qualora per un motivo qualsiasi io non ricevessi il token B, la transazione fallirebbe.
Apparentemente Uniswap contiene già una funzionalità del genere: lo slippage che mi consente di specificare la tolleranza nel rapporto di scambio A/B che sono disposto a tollerare.
Lo slippage è però implementato all’interno del codice di Uniswap, non nella piattaforma Ethereum; questo significa che:
- Non tutti gli smart contract implementano un meccanismo analogo allo slippage.
- Uno smart contract (per dolo, per imperizia dello sviluppatore, a causa di un attacco…) potrebbe non rispettarlo.
Utilizzando il wallet Radix, invece, l’utente ha facoltà di inserire tutti gli assert che vuole all’interno delle proprie transazioni ed è sicuro che sarà la piattaforma stessa a verificare che queste siano soddisfatte.
In pratica, Radix implementa le intent based transactions in modo nativo ed affidabile risolvendo definitivamente la questione del blind signing.