Nous allons, dans cet article, voir les premières bases de développement en Solidity, le langage de programmation des smart contracts sur les blockchains compatibles avec Ethereum et la Binance Smart Chain.
Avant toute chose, rappelons quelques mots de vocabulaire.
Solidity est un langage de programmation de type statique conçu pour développer des contrats intelligents qui s'exécutent sur l'EVM (Ethereum Virtual Machine). Solidity est compilé en bytecode lui-même exécutable sur l'EVM. (source : Wikipedia).
Les contrats intelligents (ou smart contracts en anglais) sont des programmes informatiques dits irrévocables. Cela signifie qu’une fois déployés sur une blockchain, ils ne peuvent plus être modifiés.
L’EVM (Ethereum Virtual Machine) est un système (sous-couche) utilisé par la plateforme Ethereum ou par la Binance Smart Chain (blockchain compatible avec Ethereum offrant les mêmes fonctions, mais dont les frais de transaction sont bien moindres). Elle est un environnement d'exécution de contrats intelligents (smart contracts). C'est en quelque sorte un ordinateur virtuel qui permet aux développeurs d’exécuter des programmes en Solidity.
Les contrats
Nous allons commencer par les bases : qu’est-ce qu’un contrat Solidity ?
Un contrat (contract) permet d’encapsuler du code Solidity. C’est en quelque sorte le backend de toutes les applications déployées sur Ethereum ou sur la Binance Smart Chain. Il contient toutes les variables et les fonctions de notre projet Solidity et sera notre point de départ.
Exemple d’un contrat vide :
pragma solidity ^0.8.19;
contract HelloWorld {
}
Tout contrat en Solidity commence toujours par la déclaration de la version du compilateur à utiliser. (Ici la version 0.8.19).
Ensuite, nous définissons l’interface du contrat en lui donnant un nom (ici : “HelloWorld”). Ce contrat est pour le moment vide et n’a aucune fonction ni variable. Nous verrons par la suite ce qu'il faut mettre dans le contrat.
Les variables
Voyons ici comment Solidity gère les variables. Les variables en Solidity sont stockées de manière permanente dans le stockage du contrat. Lorsqu’on écrit une valeur dans une variable, elle est directement écrite dans la blockchain. C’est un peu comme si on écrivait dans une base de données. Voici les principaux types de variables gérés par Solidity :
- Les entiers signés (int)
- Les entiers non signés (uint)
- Les booléens (bool)
- Les adresses (address)
- Les chaînes de caractères (string)
- etc.
Vous avez peut-être remarqué que les nombres à virgule (float) n’ont pas été cités, c’est parce qu’ils ne sont pas gérés nativement en Solidity. Il est possible d’utiliser des nombres à virgule en important des librairies mais nous verrons cela en détail plus tard.
Exemples de variables :
contract HelloWorld {
uint unsignedInteger = 1;
int integer = -1;
bool booleen = true;
address myAddress = 0xb794F5eA0ba39494cE839613fffBA74279579268;
string name = "Hugo";
}
La différence entre un entier signé (int) et un entier non signé (uint) est que l’entier signé peut être de valeur négative alors qu’un entier non signé est toujours positif. Il y a aussi une notion de taille, int et uint sont respectivement des alias pour int256 et uint256 pour 256 bits. Mais il existe aussi les int8, int32, int64, etc jusqu’à 256.
Les opérations mathématiques sur les entiers
Avec Solidity, il est possible d’effectuer des opérations mathématiques sur les nombres/variables. Les opérations disponibles sont assez simples, elles sont les mêmes que dans la plupart des langages de programmation, voici ce que nous avons à disposition :
- Les additions : x + y
- Les soustractions : x - y
- Les multiplications : x * y
- Les divisions : x / y
- Le modulo : x % y
- L’opérateur exponentiel (x à la puissance y) : x ** y
Exemple :
uint deux = 2;
uint x = 5 ** deux; // égal à 5^2 = 25
Les structures
Il est possible en Solidity de créer son propre type de variable, dans le cas où vous avez besoin d’un type de données plus complexe. Solidity nous fournit les structures (struct) de la manière suivante :
struct Person {
uint age;
string name;
}
Person personne1;
Person personne2 = Person(21, "John Doe");
Les tableaux
Il est possible, en Solidity, de regrouper des éléments dans des tableaux (array). Les tableaux en Solidity peuvent être fixes ou dynamiques. Voici un exemple de tableaux :
// Tableau avec une longueur fixe de 2 éléments :
uint[2] fixedArray;
// Un autre Tableau fixe, qui peut contenir 5 `string` :
string[5] stringArray;
// un Tableau dynamique, il n'a pas de taille fixe, il peut continuer de grandir :
uint[] dynamicArray;
Les tableaux peuvent également être combinés avec les structures vues précédemment :
Person[] people; // Tableau dynamique, on peut en rajouter sans limite.
Comme indiqué dans la partie sur les variables, toutes les variables sont stockées définitivement dans la blockchain. On peut donc facilement se servir des tableaux pour stocker des données structurées dans un contrat, un peu comme une base de données.
Pour ajouter un élément dans un tableau, Solidity fournit la fonction native push qui, comme son nom l’indique, permet de pousser un élément dans un tableau.
Exemple :
// Créer une nouvelle Personne :
Person john = Person(21, "John Doe");
// L'ajouter au tableau :
people.push(john);
Il est aussi possible de l'écrire sur une seule ligne :
people.push(Person(21, "John Doe"));
La fonction push des tableaux Solidity ajoute toujours l’élément à la fin du tableau, les éléments sont toujours rangés selon l’ordre d’ajout.
Exemple :
uint[] numbers;
numbers.push(5);
numbers.push(10);
numbers.push(15);
// numbers est maintenant égal à [5, 10, 15]
Le mot clé public pour une variable ou un tableau
Il est possible de déclarer une variable ou un tableau comme public en Solidity. Cela va permettre de créer automatiquement une méthode d’accès aux données en lecture uniquement. Par exemple si on reprend le tableau people précédent, la syntaxe serait :
Person[] public people;
Les autres contrats/utilisateurs vont pouvoir grâce à ce mot clé lire dans notre tableau de personnes (mais en aucun cas écrire). Il s’agit d’une manière de stocker de l’information publiquement (qui peut être lue par tout le monde) dans notre contrat Solidity.
Les fonctions
Les fonctions sont le cœur des contrats Solidity. Elles sont publiques par défaut, ce qui signifie que n’importe quel utilisateur ou n’importe quel contrat peut appeler la fonction de notre contrat et exécuter son code si aucun mot clé de visibilité de fonction n’est renseigné.
Il est recommandé, lorsqu’on fait du développement en Solidity de toujours marquer ses fonctions avec le mot clé private pour qu’elles ne soient pas exposées à tout le monde. Il est ensuite possible de les rendre publiques, avec le mot clé public, les fonctions souhaitées uniquement. Une déclaration de fonction classique ressemble à ça :
function addPerson(uint _age, string _name) public {}
Cette fonction vide se nomme addPerson et prend 2 paramètres : un entier non signé (age) et une chaîne de caractères (name). Il est ainsi possible d’appeler la fonction de cette manière :
addPerson(21, "John Doe");
On peut également remplir la fonction pour lui faire ajouter la personne passée en paramètre à notre tableau de structures de personnes :
function addPerson(uint _age, string _name) public {
people.push(Person(_age, _name));
}
Pour rendre notre fonction addPerson privée, il suffit d’utiliser le mot clé private comme ceci :
function addPerson(uint _age, string _name) private {
people.push(Person(_age, _name));
}
Cela permettra uniquement aux autres fonctions de notre contrat d’appeler cette fonction ajoutant ainsi une nouvelle personne au tableau “people”.
Une fonction peut d’ailleurs retourner une valeur. Il faut pour cela la déclarer dans la fonction.
Exemple :
string hw = "Hello World!";
function sayHelloWorld() public returns (string) {
return hw;
}
Nous avons ici indiqué que notre fonction était publique et qu’elle retournait une chaîne de caractères (string).
Autre spécificité, une fonction peut retourner plusieurs valeurs de différents types, voici comment gérer cela :
function multipleReturns() returns(uint a, uint b, uint c) {
return (1, 2, 3);
}
function processMultipleReturns() {
uint a;
uint b;
uint c;
// C'est comme ça que vous faites une affectation multiple :
(a, b, c) = multipleReturns();
}
La première fonction va retourner trois entiers non signés (positifs) : a, b et c. La seconde fonction “processMultipleReturns” crée 3 variables locales et les affecte en appelant la fonction “multipleReturns”. C’est aussi simple que ça.
Les événements
Un événement est un moyen pour notre contrat d’indiquer que quelque chose s’est produit. La cible peut très bien être une application Frontend par exemple. L’application Frontend serait à l’écoute de certains événements pour effectuer des traitements quand ils se produisent.
Exemple :
// Déclaration de l'événement
event PersonAdded(uint age, string name);
function addPerson(uint _age, string _name) private {
people.push(Person(_age, _name));
// Déclenchement de l'événement
PersonAdded(_age, _name);
}
Ici nous déclenchons l’événement PersonAdded à chaque fois que nous ajoutons une nouvelle personne en utilisant la fonction addPerson.
Une application Frontend écrite en Javascript pourrait très bien être en attente de l’événement et effectuer un traitement à chaque fois qu’on ajoute une personne.
Les adresses
Comme nous avons pu le voir dans le chapitre sur les variables, il est possible d’avoir comme type de variable une adresse, mais de quoi s’agit-il ?
Les blockchains (comme Ethereum et Binance Smart Chain) sont constitués de comptes, ces comptes sont assez identiques à des comptes en banque, ils possèdent un montant de la cryptomonnaie référente de la blockchain (Ether pour Ethereum et BNB pour Binance Smart Chain) et peuvent aussi posséder d’autres crypto-monnaies, NFTs, etc.
Chaque compte est représenté par une adresse unique qui sera votre identifiant de compte. Vous pouvez envoyer et recevoir des crypto-monnaies avec un portefeuille (comme Metamask par exemple) et votre adresse. Une adresse peut aussi appartenir à un smart contract, on dit alors que le smart contract est déployé à cette adresse. L’adresse en Solidity est donc l’ID unique des comptes et des smart contracts des utilisateurs.
Exemple d’une adresse : 0x0D096A5B3b8185BD6834326bD1A7221C3c331A66
Les mappings
Les mappings sont une autre façon d’enregistrer des données en Solidity, tout comme les structures et les tableaux vus précédemment. Un mapping correspond à une sorte de HashMap (pour les fans de Java ;) ). C’est un stockage dit “clé-valeur”. Il permet donc assez simplement et nativement de stocker et de rechercher des données.
Exemple de mapping :
// variable contenant mon adresse
address myAddress = 0x0D096A5B3b8185BD6834326bD1A7221C3c331A66;
// définition du mapping qui contient la balance d'un utilisateur
mapping (address => uint) public accountBalance;
// On définit la balance de notre adresse à 1000
accountBalance[myAddress] = 1000;
Dans l’exemple ci-dessus, nous avons défini dans une variable myAddress notre adresse sur la blockchain. Nous avons ensuite défini le mapping accountBalance qui prend en clé une adresse et en valeur retourne un entier non signé (positif) qui va représenter la balance de notre compte sur la blockchain. On définit ensuite la balance de notre adresse sur 1000 de base. Des fonctions pourrons ensuite ajouter, lire ou modifier la ou les balances en fonction de ce que vous souhaitez implémenter.
La variable globale msg.sender
Solidity met en place des variables globales utilisables lorsqu’on développe un contrat. La variable globale utilisée le plus souvent en Solidity et dont vous aurez certainement besoin est msg.sender. Cette variable retourne l’adresse de l’utilisateur qui a appelé notre fonction.
Par exemple, si l’on souhaite créer une fonction qui retourne la balance d’un utilisateur sans avoir à passer l’adresse de l’utilisateur en paramètre mais en utilisant son adresse avec laquelle il nous appelle, c’est quelque chose de très simple en Solidity :
// définition du mapping qui contient la balance d'un utilisateur
mapping (address => uint) public accountBalance;
function getMyBalance() public {
return accountBalance[msg.sender];
}
La fonction getMyBalance va lire dans le mapping accountBalance afin de retourner la balance de l’utilisateur qui appelle la fonction, tout ça grâce à la variable globale msg.sender. Une fonction publique en Solidity est toujours appelée de l’extérieur. Un contrat reste dans la blockchain à ne rien faire jusqu’à ce que quelqu’un appelle une de ses fonctions. Il y a donc toujours dans 100% des cas un msg.sender. Il n’est pas possible de modifier la variable globale msg.sender avant d’appeler une fonction. Elle correspond à l’adresse liée à notre compte. La seule manière pour quelqu’un d’appeler le contrat avec un msg.sender de quelqu’un d’autre serait de lui voler sa clé privée associée à son compte. Utiliser msg.sender est ainsi parfaitement sécurisé.
L’instruction require
L’instruction require est une instruction native plutôt simple à comprendre. Elle permet de tester une condition (comme un if). Si la condition est remplie, on passe à la suite. Si la condition n’est pas remplie, on s'arrête là, on ne passe pas à la suite et on renvoie une erreur.
Elle peut servir par exemple à être sûr que l’utilisateur n’appelle la fonction qu’une seule fois. Cela peut être utile dans le cas (par exemple) où nous sommes sur un contrat où l’on peut créer un objet et que l'on souhaite s'assurer que chaque utilisateur ne puisse créer qu’un seul objet et pas une infinité. Voici comment implémenter cela :
mapping (address => uint) numberOfCall;
function createObject() public {
require(numberOfCall[msg.sender] == 0);
// Suite de la fonction qui va créer l'objet ...
numberOfCall[msg.sender]++;
}
La suite de la fonction createObject n’est jamais exécutée si le nombre d’appel de msg.sender n’est pas égale à 0. A la fin de la fonction, on va incrémenter le nombre d’appel de msg.sender pour qu’il ne puisse pas exécuter la fonction une seconde fois.
Les déclarations conditionnelles (if-else) et les boucles (for et while)
Les déclarations conditionnelles en Solidity sont les mêmes qu’en Javascript, nous pouvons utiliser le if, le else if et le else. Rien de compliqué à ce niveau là, tout peut être compris avec un simple exemple :
function foo(uint x) public returns (uint) {
if (x < 10) {
return 0;
} else if (x < 20) {
return 1;
} else {
return 2;
}
}
Cette fonction retourne 0 si le paramètre d’entrée x est plus petit que 10, 1 si x est plus petit que 20 et 2 sinon. Les opérateurs sont les mêmes qu’en Javascript (==, !=, <=, >=, etc.).
Pour les boucles, la boucle for et la boucle while sont disponibles en Solidity, elle ressemble à beaucoup de langages. Voyons cela par l’exemple :
function loop() public {
for (uint i = 0; i < 10; i++) {
if (i == 3) {
// Skip to next iteration with continue
continue;
}
if (i == 5) {
// Exit loop with break
break;
}
}
uint j;
while (j < 10) {
j++;
}
}
Les deux boucles ci-dessus prennent des variables locales et les incrémentes : i pour la boucle for et j pour la boucle while. La boucle for incrémente i jusqu’à ce qu’il arrive à 5, on sort ensuite de la boucle avec une instruction break. La boucle while, elle, incrémente j jusqu’à ce que la valeur de j soit à 10. On sort ensuite de la boucle par la condition (j < 10).
Conclusion
Nous avons pu voir dans cet article les grandes bases des instructions en Solidity, vous avez mis un premier pas dans le monde de Solidity et de la blockchain mais il reste encore beaucoup de choses à apprendre comme par exemple comment déployer un contrat sur la blockchain (qui sera présenté dans un prochain article).
Cet article à été réalisé grâce à l’aide du tutoriel Solidity Crypto Zombies (https://cryptozombies.io/fr/) que je vous conseille de réaliser dès que vous aurez un peu de temps à consacrer à Solidity, il est très complet et va plus loin que les bases que vous avez vues dans cet article. Si le sujet vous intéresse et que vous voulez mettre un premier pied dedans, cela sera votre meilleur point de départ.
Sources: