Solidity è un linguaggio orientato agli oggetti pensato per implementare smart contract sulla blockchain Ethereum e in particolare sulla Ethereum Virtual Machine (EVM).
E’ un linguaggio fortemente influenzato da C++, quindi è fortemente tipizzato, con ereditarietà e permette la definizione di tipi forti definiti dall’utente.
Un contratto in Solidity è un insieme di funzioni e dati (assomigliano quindi ad una classe di C++) che risiede su uno specifico address sulla blockchain Ethereum.
Il linguaggio
Header
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
Tutti gli header degli script di Solidity iniziano con l’indicazione della licenza (in formato standard leggibile da macchina). Dato che sono script che per definizione saranno pubblici, indicare la licenza è importante.
La riga seguente indica le versioni di Solidity con cui questo script è compatibile.
Variabili di stato
Prendiamo la seguente riga di codice:
uint data;
Questa riga indica una variabile chiamata data
di tipo uint
. Fin qui tutto bene, come in C.
La differenza rispetto ai normali linguaggi di programmazione è che tale variabile non è salvata su una ram, ma è salvata sulla blockchain, come se fosse su un database.
Una volta impostata (da una funzione dello smart contract), tale variabile rimarrà perennemente a tale valore fino a che non verrà modificata.
Variabili globali
Quando si sviluppa uno smart contract è possibile accedere sempre a delle variabili standard.
- block.timestamp: valore unix dell’ora corrente. Per “corrente” intendo quando il blocco dove verrà aggiunta la mia transazione viene aggiunto alla chain
- msg.sender: address di chi sta chiamando lo smart contract
- msg.gas: gas della transazione rimanente in quel punto della funzione
- msg.value: messaggio del mittente
- this: indirizzo dello smart contract corrente.
- this.balance: quanti ether sono posseduti dallo smart contract (in wei)
Tipi o keyword
Address
In Solidity posso utilizzare un nuovo tipo di variabile, chiamato address
, che indica un indirizzo, di uno smart contract o di un wallet.
Questo tipo di dato è estremamente comodo per gestire chi possiede una determinata di quantità di coin o quanto viene gestito dallo smart contract.
public
uint public data;
La keyword public
associata ad una variabile di stato permette di creare una funzione implicita per accedere al valore di tale variabile fuori dal contratto (quindi tipicamente da parte di interfacce web o altri smart contract).
Mapping
Il mapping è il corrispettivo in Solidity dei dizionari (o hash tables) per cui ho una struttura dati chiave-valore.
mapping (address => uint) public saldi;
Per esempio nella riga seguente creo un dizionario chiave valore in cui mappo indirizzi con un numero che, per esempio, potrebbero indicare il numero di coin possedute da tale indirizzo.
Event
Gli event permettono di comunicare ai listener dello smart contract (tipicamente applicazioni web) che un determinato evento è avvenuto all’interno dello smart contract.
event Sent(address wallet, uint data);
// Quando serve...
emit Sent(wallet, data);
Per esempio nell’esempio indicato sopra posso fornire all’interfaccia web i parametri wallet
e data
quando l’evento viene lanciato.
Per approfondire come costruire applicazioni web che possano ascoltare a tale evento approfondire web3.js.
Require
La keyword require
permette di indicare un determinato insieme di condizioni per cui una determinata funzione dello smart contract possa essere chiamata.
Per esempio con il codice seguente posso forzare che la chiamata ad una determinata funziona possa essere chiamata solo dall’address chiamato special_wallet
.
require(msg.sender == special_wallet);
Ethereum Virtual Machine (EVM)
La EVM è la virtual machine nella quale girano gli smart contract su Ethereum.
Questa è sandboxed, quindi non può accedere alla rete, ai file system o a qualsiasi altro processo.
E’ stata studiata per essere eseguita da tutti i partecipanti del network contemporaneamente
Account
Esistono due tipi di account che possono accedere ad uno smart contract, gli external account (che sono i wallet delle persone fisiche definiti da una coppia di chiavi pubblica e privata) e i contract account che sono gli address di altri smart contract.
Gli external account sono identificati univocamente dalla loro chiave pubblica mentre i contract account hanno un address che viene fornito al momento della loro creazione.
Ogni account ha un balance che è una determinata quantità di Ether: all’inizio 0 e poi può essere modificata tramite delle transaction a tale account, senza poter mai scendere sotto 0.
Inoltre ogni account possiede dello storage, che è una struttura dati contenenti delle variabili di stato. E’ in questa area che sono memorizzate le variabili di stato degli smart contract descritte sopra.
Ogni smart contract non può scrivere su alcuno storage eccetto il proprio.
Transazioni
Una transazione è un messaggio che viene inviato da un account ad un altro (sottolineo che gli account sono sia quelli degli umani che quelli degli smart contract).
Ogni transazione può avere dei dati binari (payload) e degli Ether.
Se la transazione punta ad una funzione di uno smart contract, il codice è eseguito utilizzando il payload come ingresso alla funzione.
Gas
Al fine di limitare il lavoro che deve effettuare l’infrastruttura Ethereum e al fine di ricompensare i nodi della rete ogni transazione che viene effettuata deve comprendere un determinato valore di gas.
Per gas si intende una unità di misura speciale per pagare la computazione effettuata, quindi, di fatto, quanto lavoro deve fare la EVM per eseguire una serie di istruzioni.
Maggiore sarà la complessità della funzione chiamata, maggiore sarà la quantità di gas richiesta dalla rete per poter funzionare.
Si ritorna quindi alla programmazione vecchia scuola dove le risorse sono finite ed è indispensabile ottimizzare il tutto in modo da ridurre il più possibile il gas necessario a far girare il proprio smart contract. Per poter scrivere uno smart contract economico è necessario avere una conoscenza su
come i dati vengono organizzati, modificati e scritti e quale è il costo in gas di ogni operazione.
Per esempio le operazioni di lettura e scrittura su storage sono costosissime: sstore
costa 20000 gas e sload
costa 5000 gas.
Anche se una unità misurabile, non esiste alcun token per il gas, ma questo viene pagato direttamente in Ether. Nel momento in cui effettuiamo una transazione ci viene addebitata una certa quantità di ether che sarà utilizzata automaticamente per comprare il gas necessario alla transazione.
In ogni transazione possiamo definire il gas price, che è il prezzo che intendiamo pagare in ether per ogni singola unità di gas e il gas limit, ovvero la massima quantità di gas che vogliamo comprare.
Il valore di Ether che pagherò sarà quindi il risultato della moltiplicazione del gas utilizzato per il prezzo del gas che ho definito.
Maggiore è il gas price maggiore è la probabilità che i nodi validatori della rete prendano in considerazione velocemente la transazione e la aggiungano ad un blocco della chain. Ovviamente maggiore è il gas price maggiore sarà la fee che pagherò, è quindi sempre necessario trovare il giusto compromesso.
In questo punto è evidente il problema della rete Ethereum: maggiore è il numero di utilizzatori, maggiori saranno le transazioni da validare e, conseguentemente, maggiore è il gas price minimo richiesto dai validatori per accettare una transazione invece che l’altra.
Ad oggi la rete Ethereum è utilizzatissima e conseguentemente esageratamente costosa.