Cet article est une fiche technique qui a pour but d'illustrer l'implémentation d'un pattern "Factory" de smart contracts sur la blockchain Tezos. Les exemples de code seront écrits en LIGO. Il est recommandé de comprendre les concepts de blockchain et le langage LIGO de programmation de smart contract Tezos.
Le pattern "Factory" a pour but de générer à la volée des smart contrats et de permettre des interactions simples entre ces smart contract générés.
Le pattern factory est utilisé par exemple dans un DEX (decentralised exchange) qui permet l’échange entre 2 crypto-monnaies (“swap”) à travers une plateforme décentralisée. On considère une crypto-monnaie comme un smart contract suivant un standard de token (FA1.2, FA2, TZIP-16). Dans le cas du DEX, il en découle la nécessité de pouvoir créer de nouveaux tokens et permettre d'échanger un token d’un certain type contre un token d’un autre type.
Attention: Etant donné que le DEX est un smart contract sur la blockchain Tezos, il ne peut interagir qu’avec des tokens du réseau Tezos (des smart contracts déployés sur la blockchain tezos). Ici, nous considérerons que le DEX ne permet pas un échange cross-chain (par exemple entre Ethereum et Tezos).
Afin de s’affranchir des problématiques du DEX liés aux tokens, dans cette section, nous allons implémenter ce pattern sur un cas plus simple qu’un DEX.
Dans cette section, nous allons implémenter une usine de compteurs ! Considérons une factory qui génère des compteurs, où un compteur gère une valeur et des entrypoints permettant d'incrémenter et décrémenter cette valeur.
Le code LIGO de l’exemple est implémenté en cameligo (camel-like style).
Le principe est simple , un smart contract “factory” est déployé contenant :
Chaque smart contract “instance” est produit à l’aide d’un template; c’est-à-dire d’un code de smart contract en Michelson.
Un utilisateur peut interagir directement avec n’importe quel smart contract “instance”, une fois que ce dernier a été créé à l’aide de l’entrypoint “create_instance”.
Le smart contract Counter stock un entier (notre compteur) et possède 2 entrypoints
Voici le code Michelson du smart contract Counter
On peut remarquer que la définition d’un smart contract en Michelson comporte 3 éléments :
L’instruction CREATE_CONTRACT dans le langage Michelson permet de créer un contrat (et de le déployer). La syntaxe est la suivante :
CREATE_CONTRACT { parameter ty1; storage ty2; code code1 } :: option key_hash : mutez : ty2 => operation : address
On peut remarquer que pour définir un contrat il faut définir entre { et }
On peut remarquer que pour déployer un contrat il faut définir
Enfin, on peut remarquer que l’instruction CREATE_CONTRACT retourne
Nous verrons dans la section suivante comment cette instruction peut être utilisée dans un script LIGO et comment elle est appelée**.**
Pour rappel, le langage LIGO et son transpileur permettent d’écrire du code en caml-like et de générer un smart contract en Michelson.
Dans un script en LIGO, Il est possible d'appeler du code Michelson grâce à l’annotation [%Michelson et ]. Ci-dessous, voici un exemple :
Ce script définit une variable appelée create_counter de type create_counter_func.
Le type create_counter_func est défini comme une fonction lambda qui attend comme arguments
et qui renvoie
Le code de la fonction lambda est une suite d’instructions Michelson :
((l,r),r) => l, r, r
On peut remarquer que les instructions Michelson sont placées entre les balises {| et |} en suivant la syntaxe {| <sequence_instructions> |}.
On peut également remarquer que la séquence d’instruction est transtypée en type create_counter_func en suivant la syntaxe ( {| <sequence_instructions> |} : <type> )
Dans la section précédente , nous avons défini la fonction create_counter. Maintenant voyons comment l’appeler :
Le délégué n’est pas renseigné, on utilise la valeur None du type option en LIGO**.**
Le montant transféré au contrat Counter. Dans cet exemple, on utilise Tezos.amount; c’est-à-dire la quantité de mutez envoyer dans la transaction appelant create_counter. Tezos.amount est une built-in de LIGO qui stocke une quantité de mutez associée à une transaction.
Le méta-attribut storage est utilisé pour indiquer l’état initial du storage qui est défini à 0.
L'exécution de create_counter produit une opération (transaction) et une adresse.
Voyons maintenant le code complet de notre exemple de pattern Factory de compteurs.
Dans cette section, on suppose les prérequis suivants :
Compilons le contrat avec le transpiler LIGO en tapant la commande suivante :
Cette commande devrait produire une sortie comme celle-ci, s’il n’y a pas d’erreurs.
Si c’est le cas, on peut produire notre smart contract en redirigeant la sortie dans un fichier.
La commande suivante permet de préparer le storage. Ici, on indique que le storage contient un champs services_list (un set vide) et services (une map vide).
La commande produit une version Michelson de ce storage (et sera utile lors du déploiement du contrat) :
La commande suivante permet de préparer l’appel à l’entrypoint désiré. Ici, on indique que l’entrypoint invoqué est CreateService avec comme paramètre “A” et “2”.
La commande produit une version Michelson de cet entrypoint (et sera utile lors de l’interaction avec le contrat) :
Il est possible de simuler l’exécution d’un entrypoint avec la commande dry-run. Il faut donner comme paramètre l’entrypoint et ses paramètres ainsi que l’état du storage.
Comme vous pouvez le remarquer cette commande reprend les arguments des commandes précédentes.
Cette commande produit une liste d’opérations (transaction) et l’état résultant du storage.
Note, en général il est bon de tester son code en le simulant, mais dans notre cas cette commande échouera car il n’y a pas de manager d’opération. Pas de panique, nous la testerons directement en ligne de commande une fois le contrat déployé.
Le déploiement du contrat se fait en ligne de commande (CLI) à l’aide de la commande originate.
Cette commande simule l'exécution grâce à l’option --dry-run. La sortie dans la console indique la quantité de tez nécessaire à son exécution. On peut enfin réellement déployé le contrat en indiquant l’option --burn-cap.
Cette commande est asynchrone est reste en attente jusqu’à ce qu'un baker la prenne en compte. Pour cela, il faut lancer un deuxième terminal , (optionnel) réactiver l’outil tezos-client (cd tezos & eval `./src/bin_client/tezos-init-sandboxed-client.sh 1`), et lancer la commande pour valider la transaction.
Une fois la transaction validée, il est possible de voir l’adresse du contrat en exécutant la commande suivante:
Il est également possible de demander l’état du storage avec la commande suivante :
Maintenant, nous pouvons interagir avec le smart contract factory en :
Une fois qu’un compteur a été créé, il est possible d’interagir directement avec le smart contract Counter via les entrypoints Increment et Decrement.
La commande suivante permet de simuler notre entrypoint CreateService en créant un compteur A avec comme valeur initiale 2.
Pour réellement l'exécuter il faut remplacer l’option --dry-run par l’option --burn-cap (et la quantité indiquée dans la console); et valider la transaction avec la commande bake.
La commande suivante permet de simuler notre entrypoint IncrementService en incrémentant le compteur A par 5.
Il est possible de visualiser le contenu d’un storage grâce à la commande get contract storage. Dans notre exemple, cela permet de connaître les adresses associées aux différentes instances de Counter.
Cette commande produit une sortie comme celle ci-dessous. Ici, deux compteurs ont été précédemment créés.
Maintenant , on peut appeler directement le smart contract du compteur B.
Il est possible de visualiser le storage de notre compteur B avec la commande suivante, ce qui nous permettra de vérifier que tout fonctionne bien.
La commande suivante permet d’appeler l’entrypoint Increment du compteur B, dans notre cas on incrémente par 2.
La commande suivante permet d’appeler l’entrypoint Decrement du compteur B, dans notre cas on décrémente par 7.
Dans notre exemple, le smart contract factory interagit avec un compteur à l’aide de la fonction get_contract_opt qui spécifie les entrypoints possibles. Ceci implique que le type de compteur ne peut pas évoluer (il n’est pas possible de faire une seconde version du smart contract Counter avec un nouvel entrypoint Reset) car les signatures des smart contracts seraient différentes.
Pour remédier à ce problème il faudrait modifier le code du smart contract en utilisant get_entrypoint_opt à la place de get_contract_opt (et en spécifiant le nom de l’entrypoint). Par exemple en remplaçant Tezos.get_contract_opt par Tezos.get_entrypoint_opt "%increment".