Tezos - LIGO patterns - Factory

le 23/08/2021 par Frank Hillard
Tags: Évènements

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.

Définition

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.

Cas d’utilisation

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).

Implémentation

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).

Architecture de contrats

Le principe est simple , un smart contract “factory” est déployé contenant :

  • un entrypoint “create_instance” qui crée un smart contrat “instance”
  • un entrypoint “call” qui permet d’interagir simultanément avec les smart contracts “instance”.

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 template Counter

Le smart contract Counter stock un entier (notre compteur) et possède 2 entrypoints

  • Increment qui permet d’incrémenter le compteur
  • Decrement qui permet de décrémenter le compteur

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 :

  • les entrypoints du smart contract
  • la définition de la structure du storage
  • le code à exécuter

L’instruction CREATE_CONTRACT de Michelson

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 }

  • les entrypoints du smart contract
  • la définition de la structure du storage
  • le code à exécuter

On peut remarquer que pour déployer un contrat il faut définir

  • un délégué optionnel (key_hash)
  • une quantité de mutez qui sera transférée au contrat généré.
  • l’état initial du storage associé au contrat généré

Enfin, on peut remarquer que l’instruction CREATE_CONTRACT retourne

  • une opération (transaction de création du smart contract)
  • l'adresse associée smart contract

Nous verrons dans la section suivante comment cette instruction peut être utilisée dans un script LIGO et comment elle est appelée**.**

Utilisation de Michelson en LIGO

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

  • un délégué optionnel (key_hash)
  • la quantité de mutez à transférer
  • l’état initial du storage

et qui renvoie

  • la transaction permettant de déployer un nouveau smart contract Counter
  • l’adresse associée à ce compteur

Le code de la fonction lambda est une suite d’instructions Michelson :

  1. UNPPAIIR : dé-groupe une paire de paire en 3 éléments séparés

((l,r),r) => l, r, r

  1. CREATE_CONTRACT : prend en paramètre une définition de smart contract. Le smart contract Counter est utilisé comme template en injectant le code à l’aide de l’instruction #include <path_tz_file>.
  2. PAIR : regroupe 2 éléments dans une paire

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> )

Exécution de la transaction de création d’un nouveau Counter

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.

Exemple (code complet)

Voyons maintenant le code complet de notre exemple de pattern Factory de compteurs.

Déploiement et interaction avec le smart contract

Dans cette section, on suppose les prérequis suivants :

  • Tezos est installé à la racine du compte (/home/{user}). (instructions d’installation)
  • Un noeud est lancé, par exemple, en mode sandbox et le client tezos-client est accessible. Les instructions pour lancer un noeud en mode sandbox sont disponibles ici.
  • Le compilateur LIGO est installé (instructions d’installation).

Compilation

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.

Préparation

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) :

Simulation

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é.

Déploiement

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 :

Interactions

Maintenant, nous pouvons interagir avec le smart contract factory en :

  • créant un nouveau compteur avec l’entrypoint CreateService
  • Incrémentant un des compteurs via le smart contract factory avec l’entrypoint IncrementService
  • Décrémentant un des compteurs via le smart contract factory avec l’entrypoint DecrementService

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.

Création d’un compteur

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.

Modification un compteur via factory

La commande suivante permet de simuler notre entrypoint IncrementService en incrémentant le compteur A par 5.

Lister les compteurs (et leur adresses)

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.

Modification un compteur directement

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.

Limitation

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".