--- language: Solidity filename: learnSolidity.sol contributors: - ["Nemil Dalal", "https://www.nemil.com"] - ["Joseph Chow", ""] - ["Bhoomtawath Plinsut", "https://github.com/varshard"] - ["Shooter", "https://github.com/liushooter"] - ["Patrick Collins", "https://gist.github.com/PatrickAlphaC"] translators: - ["Al", "http://github.com/al-ias"] lang: it-it --- Solidity permette di programmare su [Ethereum](https://www.ethereum.org/), una macchina virtuale basata sulla blockchain che consente la creazione e l'esecuzione degli smart contract senza che sia richiesta centralizzazione o fiducia negli attori coinvolti. Solidity è un linguaggio di programmazione di contratti tipizzato staticamente e ha molte cose in comune con Javascript e C. Come per gli oggetti nella programmazione ad oggetti, ogni contratto contiene variabili di stato, funzioni e tipi di dato semplici. Tra le funzionalità specifiche dei contratti troviamo le clausole (guardie) dei modifier, gli event notifier per i listener, e le variabili globali custom. Come esempi di contratti su Ethereum troviamo sistemi di crowdfunding, voto, [finanza decentralizzata](https://defipulse.com/) e aste al buio. Compiere errori nel codice Solidity può portare a rischi e costi alti, quindi bisogna fare attenzione a testare e rilasciare le modifiche lentamente. A CAUSA DEI CONTINUI CAMBIAMENTI DI ETHEREUM È IMPROBABILE CHE QUESTO DOCUMENTO RESTI AGGIORNATO, QUINDI COSNIGLIAMO DI SEGUIRE LA CHAT ROOM DI SOLIDITY E IL BLOG DI ETHEREUM PER TENERSI AGGIORNATI. TUTTO IL CODICE QUI PRESENTE E' FORNITO COSÌ COM'È, CON ANNESSI RISCHI SOSTANZIALI DI ERRORI O PATTERN DI PROGRAMMAZIONE DEPRECATI. A differenza di altri tipi di codice, potresti aver bisogno di usare pattern di pausing, deprecation e throttling usage per ridurre il rischio. Questo documento tratta principalmene la sintassi e quindi esclude molti design pattern in voga. Visto che Solidity e Ethereum sono in continuo sviluppo, le funzionalità sperimentali o beta sono evidenziate e soggette a cambiamenti. Ogni Pull Request è ben accetta. # Lavorare con Remix e Metamask Uno dei modi più semplici di scrivere, distribuire e testare il codice Solidity è usare : 1. [L'ambiente di sviluppo online Remix](https://remix.ethereum.org/) 2. [Il wallet Metamask](https://metamask.io/). Per cominciare, [scarichiamo l'estesione per browser di Metamask](https://metamask.io/). Una volta installata, potremo iniziare ad usare Remix. Il codice seguente è pre-inizializzato, ma prima di addentrarci, diamo un'occhiata a qualche trucchetto per iniziare ad usare Remix. Carica tutto il necessario [clickando su questo link](https://remix.ethereum.org/#version=soljson-v0.6.6+commit.6c089d02.js&optimize=false&evmVersion=null&gist=f490c0d51141dd0515244db40bbd0c17&runs=200). 1. Scegli il compilatore per Solidity ![Solidity-in-remix](../images/solidity/remix-solidity.png) 2. Apri il file che si caricherà su quel link ![Solidity-choose-file](../images/solidity/remix-choose-file.png) 3. Compila il file ![Solidity-compile](../images/solidity/remix-compile.png) 4. Fai il deploy ![Solidity-deploy](../images/solidity/remix-deploy.png) 5. Smanetta con i contratti ![Solidity-deploy](../images/solidity/remix-interact.png) Hai distribuito il tuo primo contrattto! Congratulazioni! Potrai testarlo e smanettare con le funzioni già definite. Dai un'occhiata ai commenti per scoprire cosa fanno. ## Lavorare su una testnet Distribuire e testare su una testnet è il modo più accurato per mettere alla prova i tuoi smart contract in Solidity. Per farlo procuriamoci prima degli ETH di test dalla testnet Kovan. [Entra in questo Gitter Channel](https://gitter.im/kovan-testnet/faucet) e scrivici l'indirizzo del tuo wallet Metamask. Sul tuo Metamask, dovrai cambiare la testnet in `Kovan`. ![Solidity-in-remix](../images/solidity/metamask-kovan.png) Riceverai degli Ethereum di test gratuiti. Per distribuire degli smart contract su una testnet abbiamo bisogno di Ethereum. Nell'esempio precedente non avevamo usato una testnet, ma avevamo distribuito su un ambiente virtuale fittizio. Quando si lavora su una testnet, possiamo davvero monitorare e interagire con i nostri contratti in maniera persistente. Per distribuire su una testnet, allo step `#4 Fai il deploy`, cambia l'`environment` selezionato in `injected web3`. In questo modo verrà usato come network su cui fare il deploy qualsiasi network selezionato sul tuo Metamask. ![Solidity-in-remix](../images/solidity/remix-testnet.png) Per ora continua a usare la `Javascript VM` a meno che non ti sia detto di cambiarla. Quando distribuisci su una testnet, Metamask aprirà un pop up che ti chiederà di "confermare" la transazione. Premi `yes` e dopo un certo lasso di tempo, ti apparirà la stessa interfaccia per il contratto nella parte inferiore dello schermo. ```javascript // Iniziamo con un semplice contratto su una Banca // Permette di depositare, prelevare e fare l'estratto conto // simple_bank.sol (nota l'estensione .sol) /* **** INIZIO DELL'ESEMPIO **** */ // Dichiara la versione del compilatore per il file sorgente pragma solidity ^0.6.6; // Inizia con il commento Natspec (i tre slash) // viene usato per la documentazione - e per i dati descrittivi per gli elementi // dell'interfaccia utente / azioni /// @title SimpleBank /// @author nemild /* 'contract' somiglia a 'class' in altri linguaggi (ha variabili di classe, ereditarietà, etc.) */ contract SimpleBank { // CapWords // Dichiariamo le variabili di stato fuori dalle funzioni, persisteranno // durante tutta la vita del contratto // i dizionari mappano gli indirizzi con i saldi // fai sempre attenzione agli overflow attack che sfruttano i numeri mapping (address => uint) private balances; // "private" significa che che altri contratti non possono leggere i // saldi ma le informazioni restano visibili ad altri attori sulla blockchain address public owner; // 'public' lo rende leggibile dall'esterno (ma non modificabile) dagli // utenti e dai contratti // Gli 'event' pubblicano le azioni in modo che siano ascoltabili da // listener esterni event LogDepositMade(address accountAddress, uint amount); // I 'constructor' possono ricevere una o più parametri; Si può // dichiarare un solo costruttore constructor() public { // 'msg' fornisce i dettagli sul messaggio che è stato mandato al contratto // 'msg.sender' è chi invoca il contratto (l'indirizzo di chi lo crea) owner = msg.sender; } /// @notice Deposita ether nella banca /// @return Il saldo dell'utente dopo che è stato effettualto il deposito function deposit() public payable returns (uint) { // Usiamo 'require' per testare gli input dell'utente, 'assert' per gli // invarianti interni. Qui ci assicuriamo di non avere a che fare con // un overflow require((balances[msg.sender] + msg.value) >= balances[msg.sender]); balances[msg.sender] += msg.value; // Non servono "this." o "self." con le variabili di stato // Tutti i valori iniziali delle variabili sono impostati automaticamente // al valore di default per quel tipo di dato emit LogDepositMade(msg.sender, msg.value); // Fa scattare l'evento return balances[msg.sender]; } /// @notice Preleva ether dalla banca /// @dev Non restituisce gli ether inviati in eccesso /// @param withdrawAmount L'importo che si vuole ritirare /// @return remainingBal function withdraw(uint withdrawAmount) public returns (uint remainingBal) { require(withdrawAmount <= balances[msg.sender]); // Notiamo come per prima cosa scaliamo i soldi dal saldo, prima di // invarli. Ogni .transfer/.send in questo contratto può chiamare una // funzione esterna. Questa cosa potrebbe permettere a chi invoca la // funzione di richiedere un importo maggiore del suo saldo usando // una chiamata ricorsiva. Miriamo ad aggiornare lo stato prima che sia // chiamata una funzione esterna, incluse .transfer/.send balances[msg.sender] -= withdrawAmount; // Qui lancia automaticamente un errore se fallisce, il che implica // che il saldo (non più aggiornato) viene ripristinato a prima della // transazione msg.sender.transfer(withdrawAmount); return balances[msg.sender]; } /// @notice Recupera il saldo /// @return Il saldo dell'utente // 'view' (ex: constant) impedisce alle funzioni di modificare lo stato // delle variabili; consente alle le funzioni di essere disponibili in // locale/fuori dalla blockchain function balance() view public returns (uint) { return balances[msg.sender]; } } // ** FINE DELL'ESEMPIO ** // Passiamo alle basi di Solidity // 1. TIPI DI DATO E I LORO METODI // uint viene usato per gli importi in valuta (non ci sono double o float) // e per le date (in unix time) uint x; // int di 256 bit, non possono essere modificati dopo l'istanziazione int constant a = 8; int256 constant a = 8; // stesso effetto della riga prima, qui viene // dichiarato esplicitamente che è di 256 bit uint constant VERSION_ID = 0x123A1; // Una costante esadecimale // con 'constant', il compilatore rimpiazza ogni occorrenza con il valore // Tutte le variabili di stato (quelle fuori da una funzione) // sono 'interne' di default e accessibili SOLO dall'interno del contratto // e da tutti contratti che le ereditano // Bisogna usare esplicitamente 'public' per consentire l'accesso dai contratti // esterni int256 public a = 8; // Per int e uint possiamo esplicitamente assegnare una dimensione tra 8 e 256 // es. int8, int16, int24 uint8 b; int64 c; uint248 e; // Attenzione a non andare in overflow, e a proteggersi dagli attacchi che lo fanno // Ad esempio per quanto rigrada l'addizione, conviene fare: uint256 c = a + b; assert(c >= a); // 'assert' testa gli invarianti interni; require viene usato // per gli input // Per altri esempi di problemi comuni con le operazioni aritmentiche, dai una // occhiata alla Zeppelin's SafeMath library // https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/contracts/math/SafeMath.sol // Non ci sono funzioni random built-in, puoi ottenere un numero pseudo-casuale // hashando l'ultimo blockhash, o ottenere un numero realmente casuale usando // qualcosa come Chainlink VRF. // https://docs.chain.link/docs/get-a-random-number // Conversione di tipo int x = int(b); bool b = true; // oppure 'var b = true;' per l'inferenza di tipo // Indirizzi - contengono indirizzi Ethereum di 20 byte/160 bit // Non sono consentite operazioni aritmetiche address public owner; // Tipi di account: // Contract account: l'indirizzo viene impostato quando lo si crea (funzione con // l'indirzzo di chi lo crea, il numero della transazione inviata) // External Account: (persona/enitità esterna): l'indirizzo viene creato dala // chiave pubblica // Aggiungi il campo 'public' per indicare che è pubblico/accessibile dall'esterno // un getter viene creato automaticamente, ma NON un setter // Si possono mandare ether a tutti gli indirizzi owner.transfer(SOME_BALANCE); // fallisce e, in tal caso, ripristina // lo stato precedente // Possiamo anche usare la funzione di livello più basso .send, che restituisce // false se fallisce if (owner.send) {} // RICORDA: metti la send in un 'if' dato che gli indirizzi // usati nei contratti hanno delle funzioni, che vengono eseguite quando viene // fatta una send, che possono fallire // Inoltre fai attenzione a scalare i saldi PRIMA di provare a fare una send, // dato il rischio di chiamate riscorsive che potrebbero prosciugare il contratto // Possiamo controllare il saldo owner.balance; // il saldo del propietario (utente o contratto) // I Byte sono disposibili in dimensioni da 1 a 32 byte a; // 'byte' è la stessa cosa di 'bytes1' bytes2 b; bytes32 c; // Byte con dimensione dinamica bytes m; // Un array particolare, la stessa cosa dell'array 'byte[]' (ma scritto stringato) // È più dispendioso di byte1-byte32, che di solito sono preferibili // come bytes, ma non permette di accedere alla lunghezza o all'indice (per ora) string n = "hello"; // salvato in UTF8, nota i doppi apici, non singoli // le utility function per le stringhe saranno aggiunte in futuro // sono preferibili bytes32/bytes, dato che UTF8 occupa più memoria // Inferenza di tipo // 'var' fa inferenza di tipo a seconda del primo assegnamento, // non può essere usata tra i parametri di una funzione var a = true; // da usare con cautela, può inferire un tipo errato // es. un int8 quando un contatore dev'essere un int16 // var può essere usata per assegnare una funzione ad una variabile function a(uint x) returns (uint) { return x * 2; } var f = a; f(22); // chiamata // di default, tutte le variabili sono impostate a 0 durante l'istanziazione // Delete può essere chiamato sulla maggior parte dei valori // (NON distrugge il valore, ma lo setta a 0, il valore did default) uint x = 5; // Destructuring/Tuple (x, y) = (2, 7); // assegna/scambia più valori // 2. STRUTTURE DATI // Array bytes32[5] nicknames; // array statico bytes32[] names; // array dinamico uint newLength = names.push("John"); // aggiungere un elemento restituisce // la nuova dimensione dell'array // Dimesione names.length; // ottenere la dimensione names.length = 1; // la dimensione può essere assegnata (solo per gli array nello storage) // array multidimensionali uint[][5] x; // array con 5 array dinamici (ordine opposto rispetto ad // altri linguaggi) // Dizionari (da un tipo qualsiasi a un tipo qualsiasi) mapping (string => uint) public balances; balances["charles"] = 1; // il risultato balances["ada"] è 0, tutte le chiavi non settate // restituiscono zero // 'public' permette che si possa fare questo da un altro contratto: contractName.balances("charles"); // restituisce 1 // 'public' ha creato getter (ma non un setter), come il seguente: function balances(string _account) returns (uint balance) { return balances[_account]; } // Mapping annidati mapping (address => mapping (address => uint)) public custodians; // Fare una delete delete balances["John"]; delete balances; // assegna 0 a tutti gli elementi // Diversamente da altri linguaggi NON si può iterare tra gli elementi di un // mapping senza conoscere le chiavi - ma si può costruire una struttura dati a monte // che lo faccia // Strutture dati struct Bank { address owner; uint balance; } Bank b = Bank({ owner: msg.sender, balance: 5 }); // oppure Bank c = Bank(msg.sender, 5); c.balance = 5; // imposta ad un nuovo valore delete b; // reimposta, imposta tutte le variabili della struttura a 0, tranne i mapping // Enumerazioni enum State { Created, Locked, Inactive }; // di solito si usano per le macchine di stato State public state; // Dichiara una variabile da un enum state = State.Created; // Le enum possono essere convertite esplicitamente in int uint createdState = uint(State.Created); // 0 // Data location: Memory vs. storage vs. calldata - tutti i tipi complessi // (array, struct) hanno una data location // 'memory' non è persistente, 'storage' sì // Il default è 'storage' per varibili locali e di stato; // 'memory' per i parametri delle funzioni // Lo stack può contenere poche varaibili locali // Per la maggior parte dei tipi, si può impostare esplicitamente // quale data location usare // 3. Operatori semplici // Ci sono operatori logici, a bit e aritmetici // Potenza: ** // Or esclusivo: ^ // Negazione bitwise: ~ // 4. Variabili globali degne di nota // ** this ** this; // indirizzo del contratto // di solito si usa per trasferire il aldo rimanente altrove // al termine della vita del contratto this.balance; this.someFunction(); // invoca una funzione esterna tramite chiamata, // non attraverso un salto interno // ** msg - Il messaggio corrente ricevuto dal contratto ** msg.sender; // indirizzo di chi ha inviato msg msg.value; // l'importo di ether forniti a questo contratto espresso in "wei", // la funzione dovrebbe essere marcata come "payable" msg.data; // in bytes, tutti gli argomenti del messaggio msg.gas; // 'gas' restante // ** tx - Questa transazione ** tx.origin; // l'indirizzo di chi ha avviato questa transazione tx.gasprice; // il prezzo del "gas" per la transazione // ** block - Informazioni sul blocco attuale ** now; // ora corrente (approssimatamente), alias di block.timestamp (in Unix time) // Da notare come può essere manipolata dai miner, quindi da usare con cautela block.number; // numero del blocco attuale block.difficulty; // difficulty del blocco attuale block.blockhash(1); // restituisce un bytes32, funziona solo per i 256 blocchi // più recenti block.gasLimit(); // ** storage - Memoria persistente (in hash) ** storage['abc'] = 'def'; // mappa da parole di 256 bit a parole di 256 bit // 4. FUNZIONI E ALTRO // A. Funzioni // Una semplice funzione function increment(uint x) returns (uint) { x += 1; return x; } // Le funzioni possono restituire molti valori, // e visto che i valori di ritorno vengono dichiarati prima // non è richiesta un'instruzione return esplicita function increment(uint x, uint y) returns (uint x, uint y) { x += 1; y += 1; } // Chiama la funzione di cui sopra uint (a,b) = increment(1,1); // 'view' (un alias di 'constant') // indica che la funzione non cambia / non può cambiare le variabili persistenti // Le funzioni definite con view vengono eseguite localmente, non sulla blockchain // N.B. la keyword constant sarà presto deprecata uint y = 1; function increment(uint x) view returns (uint x) { x += 1; y += 1; // questa riga fallirebbe // y è una variabile di stato, e non può essere cambiata in una funzione di view } // 'pure' è più restrittivo di 'view' o 'constant', e non // permette nemmeno di leggere le varaibili di stato // In realtà è più complicato, per approfondire su // view/pure: // http://solidity.readthedocs.io/en/develop/contracts.html#view-functions // Modificatori di visibilità per le funzioni // Possono essere messi vicino a 'view' e includono: // public - visibile esternamente e internamente (di default per function) // external - visible solo esternamente (comprese le chiamate fatte con this.) // private - visibile slo dal contratto attuale // internal - visibile solo dal contratto attuale, e da quelli che ne derivano // Di solito è una buona idea marcare esplicitamente ogni funzione // Funzioni hoisted - e si può assegnare una funzione ad una variabile function a() { var z = b; b(); } function b() { } // Tutte le funzioni che ricevono ether devono essere dichiarate come 'payable' function depositEther() public payable { balances[msg.sender] += msg.value; } // I cicli sono da preferire alla ricorsione // (la profondità massima dello stack è 1024) // Inoltre, non impostare dei loop senza limiti, // perchè potresti reggiungere il limite per il gas // B. Eventi // Gli eventi notificano a terze parti; è facile ispezionare e // accedere agli eventi al di fuori della blockchain (con client leggeri); // Tipicamente si dichiarano dopo i parametri del contratto // Tipicamente, sono capitalized - si usa Log come prefisso per esplicitarli // meglio ed evitare che si confondano con una chiamata a funzione // Dichiarazione event LogSent(address indexed from, address indexed to, uint amount); // Da notare le prime lettere maiuscole // Chiamata LogSent(from, to, amount); /** Una terza parte esterna (entità o contratto), può osservare usando la libreria Javascript Web3: // Quel che se segue è codice Javascript, non Solidity Coin.LogSent().watch({}, '', function(error, result) { if (!error) { console.log("Trasferimento valuta: " + result.args.amount + " la valuta è stata mandata da " + result.args.from + " a " + result.args.to + "."); console.log("I saldi ora sono:\n" + "Mittente: " + Coin.balances.call(result.args.from) + "Destinatario: " + Coin.balances.call(result.args.to)); } } **/ // È prassi che un contratto dipenda da un altro (es. che dipenda // dai tassi di cambio forniti da un altro contratto) // C. Modifier // I modifier validano gli input per conto dele funzioni verificando ad esempio // il saldo minimo o l'autenticazione dell'utente; // sono simili alle calusole di guardia di altri linguaggi // '_' (underscore) viene spesso posizionato nell'ultima riga del body, e indica // che la funzione chiamata dev'essere posizionata lì modifier onlyAfter(uint _time) { require (now >= _time); _; } modifier onlyOwner { require(msg.sender == owner) _; } // usate comunemente negli automi a stati finiti modifier onlyIfStateA (State currState) { require(currState == State.A) _; } // Si dichiarano appena dopo la definizione di una funzione function changeOwner(newOwner) onlyAfter(someTime) onlyOwner() onlyIfState(State.A) { owner = newOwner; } // L'underscore può essere messo prima della fine del body, // ma un'istruzione di ritorno esplicita lo salterebbe, // quindi è da usare con cautela modifier checkValue(uint amount) { _; if (msg.value > amount) { uint amountToRefund = amount - msg.value; msg.sender.transfer(amountToRefund); } } // 6. ISTRUZIONI CONDIZIONALI E CICLI // Troviamo tutte le istruzioni condizionali di base - incluse if/else, for, // while, break, continue e return - ma non c'è lo switch // La sintassi è la stessa di javascript, ma non esiste la conversione di tipo // in booleano dai non booleani (bisogna usare gli operatori logici per // ottenere il valore boolean) // Bisogna stare attenti i loop che iterano in base al comportamento // dell'utente, dato che i contratti hanno un tetto massimo di gas // per blocco di codice e falliranno se lo superano // Ad esempio: for(uint x = 0; x < refundAddressList.length; x++) { refundAddressList[x].transfer(SOME_AMOUNT); } // Ci sono due errori nel codice precedente: // 1. Un fallimento su una transfer impedisce al loop di completare tutti // i cicli, bloccando dei soldi; // 2. Questo loop potrebbe essere arbitrariamente lungo (si basa sul numero // degli utenti che hanno diritto al rimborso), quindi potrebbe fallire sempre // se supera il tetto massimo di gas per blocco; // Come soluzione, si potrebbe permettere agli utenti di prelevare // individualmente dal loro subaccount e segnare il rimborso come riscosso // Ad es. preferire pull payments ai push payment // 7. OGGETTI/CONTRATTI // A. Invocare un contratto esterno contract InfoFeed { function info() payable returns (uint ret) { return 42; } } contract Consumer { InfoFeed feed; // punta ad un contratto sulla blockchain // Imposta il feed sull'istanza del contratto esistente function setFeed(address addr) { // fare attenzione alla conversione di tipo automatica; // il costruttore non viene invocato feed = InfoFeed(addr); } // Imposta il feed ad una nuova istanza del contratto function createNewFeed() { feed = new InfoFeed(); // viene creata una nuova istanza; // viene invocato il costruttore } function callFeed() { // le parentesi finali invocano il contratto, opzionalmente si può // specificare un importo custom di ether o di gas feed.info.value(10).gas(800)(); } } // B. ereditarietà // Conta l'ordine, l'ultimo contratto ereditato (es. 'def') può andare // in overriding su parti dei contratti precedentemente ereditati contract MyContract is abc, def("a custom argument to def") { // Funzione in overriding function z() { if (msg.sender == owner) { def.z(); // invoca la funzione overridden da def super.z(); // chiama la funzione overridden del padre } } } // Funzioni astratte function someAbstractFunction(uint x); // non possono essere compilate, vengono usate nei contratti base/astratti // e poi verranno implementate // C. Import import "filename"; import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol"; // 8. ALTRE KEYWORD // A. Selfdestruct // autodistrugge il contratto corrente, inviando i fondi ad un indirizzo // (di solito il creatore) selfdestruct(SOME_ADDRESS); // rimuove il codice e quanto in memoria dal blocco corrente e tutti i futuri blocchi // aiuta ad alleggerire i client, ma le informazioni precedenti continueranno // a persistere sulla blockchain // È un pattern comune, permette al proprietario di terminare il contratto // e ricevere i fondi rimasti function remove() { if(msg.sender == creator) { // Solo il creatore del contratto può farlo selfdestruct(creator); // Cessa l'attività del contratto, trasferisce i fondi } } // Si potrebbe voler disattivare il contratto manualmente, anzichè usare una // selfdestruct (gli ether inviati ad un contratto dopo una selfdestruct // vengono persi) // 9. NOTE SUL DESIGN DEI CONTRATTI // A. Offruscamento // Tutte le variabili sono pubblicamente visibili sulla blockchain, quindi // qualsiasi cosa privata ha bisogno di essere offruscata (es. hash con una // chiave segreta) // Passi: 1. Impegnarsi pagare una certa cifra, 2. Rivelare l'impegno preso keccak256("una_puntata_d_asta", "un segreto"); // impegno // in futuro, l'invocazione della funzione rivelatrice del contratto // mostrerà la puntata ed il segreto che produce lo SHA3 reveal(100, "ilMioSegreto"); // B. Ottimizzazione della memoria (storage) // Scrivere dati sulla blockchain può essere costoso visto che vengono // conservati per sempre; siamo incoraggati ad usare la memoria in maniera // scaltra (un giorno la compilazione migliorerà, ma per ora è vantaggioso // pianificare le strutture dati da usare - e conservarne il minimo possibile // sulla blockchain) // I costi per conservare cose come array multidimensionali sono spesso alti // (costa conservare dati - non dichiarare variabili parzialmente vuote) ```