Contrats Ethereum Oracle : fonctionnalités du code de solidité
Publié: 2022-03-11Dans le premier segment de ces trois parties, nous avons parcouru un petit tutoriel qui nous a donné une simple paire contrat-avec-oracle. Les mécanismes et processus de configuration (avec truffle), de compilation du code, de déploiement sur un réseau de test, d'exécution et de débogage ont été décrits ; cependant, de nombreux détails du code ont été passés sous silence à la main. Alors maintenant, comme promis, nous allons examiner certaines de ces fonctionnalités linguistiques qui sont uniques au développement de contrats intelligents Solidity et uniques à ce scénario contrat-oracle particulier. Bien que nous ne puissions pas examiner minutieusement chaque détail (je vous laisserai cela dans vos études ultérieures, si vous le souhaitez), nous essaierons de trouver les caractéristiques les plus frappantes, les plus intéressantes et les plus importantes du code.
Afin de faciliter cela, je vous recommande d'ouvrir soit votre propre version du projet (si vous en avez une), soit d'avoir le code à portée de main pour référence.
Le code complet à ce stade peut être trouvé ici : https://github.com/jrkosinski/oracle-example/tree/part2-step1
Ethereum et solidité
Solidity n'est pas le seul langage de développement de contrats intelligents disponible, mais je pense qu'il est assez sûr de dire que c'est le plus courant et le plus populaire en général, pour les contrats intelligents Ethereum. C'est certainement celui qui a le support et les informations les plus populaires, au moment d'écrire ces lignes.
Solidity est orienté objet et Turing-complet. Cela dit, vous réaliserez rapidement ses limitations intégrées (et entièrement intentionnelles), qui rendent la programmation de contrats intelligents très différente du piratage ordinaire.
Version solidité
Voici la première ligne de chaque poème du code Solidity :
pragma solidity ^0.4.17;
Les numéros de version que vous voyez vont différer, car Solidity, encore dans sa jeunesse, change et évolue rapidement. La version 0.4.17 est la version que j'ai utilisée dans mes exemples ; la dernière version au moment de cette publication est la 0.4.25.
La dernière version en ce moment où vous lisez ceci pourrait bien être quelque chose de complètement différent. De nombreuses fonctionnalités intéressantes sont en préparation (ou du moins prévues) pour Solidity, dont nous parlerons tout à l'heure.
Voici un aperçu des différentes versions de Solidity.
Conseil de pro : vous pouvez également spécifier une plage de versions (bien que je ne le fasse pas trop souvent), comme ceci :
pragma solidity >=0.4.16 <0.6.0;
Fonctionnalités du langage de programmation Solidity
Solidity a de nombreuses fonctionnalités de langage qui sont familières à la plupart des programmeurs modernes ainsi que certaines qui sont distinctes et (pour moi du moins) inhabituelles. On dit qu'il a été inspiré par C++, Python et JavaScript, qui me sont tous familiers personnellement, et pourtant Solidity semble assez distinct de l'un de ces langages.
Contracter
Le fichier .sol est l'unité de base du code. Dans BoxingOracle.sol, notez la 9ème ligne :
contract BoxingOracle is Ownable {
Comme la classe est l'unité de base de la logique dans les langages orientés objet, le contrat est l'unité de base de la logique dans Solidity. Qu'il suffise de simplifier pour l'instant pour dire que le contrat est la "classe" de Solidity (pour les programmeurs orientés objet, c'est un saut facile).
Héritage
Les contrats de solidité prennent pleinement en charge l'héritage, et cela fonctionne comme prévu ; les membres sous contrat privés ne sont pas hérités, alors que les membres protégés et publics le sont. La surcharge et le polymorphisme sont pris en charge comme vous vous en doutez.
contract BoxingOracle is Ownable {
Dans la déclaration ci-dessus, le mot-clé "est" désigne l'héritage. BoxingOracle hérite de Ownable. L'héritage multiple est également pris en charge dans Solidity. L'héritage multiple est indiqué par une liste de noms de classes délimités par des virgules, comme ceci :
contract Child is ParentA, ParentB, ParentC { …
Alors que (à mon avis) ce n'est pas une bonne idée d'être trop complexe lors de la structuration de votre modèle d'héritage, voici un article intéressant sur la solidité en ce qui concerne le soi-disant problème de diamant.
Énumérations
Les énumérations sont prises en charge dans Solidity :
enum MatchOutcome { Pending, //match has not been fought to decision Underway, //match has started & is underway Draw, //anything other than a clear winner (eg, cancelled) Decided //index of participant who is the winner }
Comme vous vous en doutez (pas différent des langages familiers), chaque valeur enum se voit attribuer une valeur entière, commençant par 0. Comme indiqué dans la documentation Solidity, les valeurs enum sont convertibles en tous les types entiers (par exemple, uint, uint16, uint32, etc.), mais la conversion implicite n'est pas autorisée. Ce qui signifie qu'ils doivent être transtypés explicitement (en uint, par exemple).
Solidity Docs : Enums Enums Tutoriel
Structures
Les structures sont un autre moyen, comme les énumérations, de créer un type de données défini par l'utilisateur. Les structures sont familières à tous les codeurs de base C/C++ et aux anciens comme moi. Un exemple de structure, à partir de la ligne 17 de BoxingOracle.sol :
//defines a match along with its outcome struct Match { bytes32 id; string name; string participants; uint8 participantCount; uint date; MatchOutcome outcome; int8 winner; }
Remarque à tous les anciens programmeurs C : le "packing" de Struct dans Solidity est une chose, mais il y a quelques règles et mises en garde. Ne présumez pas nécessairement que cela fonctionne de la même manière qu'en C ; vérifiez les documents et soyez conscient de votre situation, pour déterminer si l'emballage va vous aider ou non dans un cas donné.
Emballage de structure de solidité
Une fois créées, les structures peuvent être traitées dans votre code en tant que types de données natifs. Voici un exemple de syntaxe pour "l'instanciation" du type de structure créé ci-dessus :
Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1);
Types de données dans Solidity
Cela nous amène au sujet très basique des types de données dans Solidity. Quels types de données Solidity prend-il en charge ? Solidity est typé statiquement et, au moment de la rédaction de cet article, les types de données doivent être explicitement déclarés et liés à des variables.
Types de données de solidité
Booléens
Les types booléens sont pris en charge sous le nom bool et les valeurs true ou false
Types numériques
Les types entiers sont pris en charge, signés et non signés, de int8/uint8 à int256/uint256 (c'est-à-dire des entiers 8 bits à des entiers 256 bits, respectivement). Le type uint est un raccourci pour uint256 (et de même int est un raccourci pour int256).
Notamment, les types à virgule flottante ne sont pas pris en charge. Pourquoi pas? Eh bien, d'une part, lorsqu'il s'agit de valeurs monétaires, les variables à virgule flottante sont bien connues pour être une mauvaise idée (en général bien sûr), car la valeur peut être perdue dans les airs. Les valeurs d'éther sont notées en wei, qui est 1/1 000 000 000 000 000 000ème d'un éther, et cela doit être une précision suffisante à toutes fins; vous ne pouvez pas décomposer un éther en parties plus petites.
Les valeurs en virgule fixe sont partiellement prises en charge pour le moment. Selon les documents de Solidity : "Les nombres à virgule fixe ne sont pas encore entièrement pris en charge par Solidity. Ils peuvent être déclarés, mais ne peuvent pas être attribués à ou à partir de.
https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9
Remarque : dans la plupart des cas, il est préférable d'utiliser simplement uint, car la diminution de la taille de la variable (en uint32, par exemple) peut en fait augmenter les coûts de gaz plutôt que de les diminuer comme on pourrait s'y attendre. En règle générale, utilisez uint sauf si vous êtes certain d'avoir une bonne raison de faire autrement.
Types de chaînes
Le type de données chaîne dans Solidity est un sujet amusant ; vous pouvez avoir des opinions différentes selon la personne à qui vous parlez. Il existe un type de données de chaîne dans Solidity, c'est un fait. Mon avis, probablement partagé par la plupart, est qu'il n'offre pas beaucoup de fonctionnalités. Analyse de chaîne, concaténation, remplacement, découpage, même comptage de la longueur de la chaîne : aucune de ces choses que vous attendez probablement d'un type de chaîne n'est présente, et elles sont donc de votre responsabilité (si vous en avez besoin). Certaines personnes utilisent bytes32 à la place de string ; ça peut se faire aussi.
Article amusant sur les cordes Solidity
Mon avis : ce pourrait être un exercice amusant d'écrire votre propre type de chaîne et de le publier pour un usage général.
Type d'adresse
Unique peut-être à Solidity, nous avons un type de données d' adresse , spécifiquement pour le portefeuille Ethereum ou les adresses de contrat. C'est une valeur de 20 octets spécifiquement pour stocker des adresses de cette taille particulière. De plus, il a des membres de type spécifiquement pour les adresses de ce type.
address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;
Types de données d'adresse
Types DateHeure
Il n'y a pas de type Date ou DateTime natif dans Solidity, en soi, comme c'est le cas dans JavaScript, par exemple. (Oh non, la solidité sonne de pire en pire à chaque paragraphe ! ?) Les dates sont nativement adressées comme des horodatages de type uint (uint256). Ils sont généralement traités comme des horodatages de style Unix, en secondes plutôt qu'en millisecondes, car l'horodatage de bloc est un horodatage de style Unix. Dans les cas où vous avez besoin de dates lisibles par l'homme pour diverses raisons, des bibliothèques open source sont disponibles. Vous remarquerez peut-être que j'en ai utilisé un dans BoxingOracle : DateLib.sol. OpenZeppelin a également des utilitaires de date ainsi que de nombreux autres types de bibliothèques d'utilitaires généraux (nous verrons bientôt la fonctionnalité de bibliothèque de Solidity).
Conseil de pro : OpenZeppelin est une bonne source (mais bien sûr pas la seule bonne source) pour les connaissances et le code générique pré-écrit qui peuvent vous aider à créer vos contrats.
Mappages
Notez que la ligne 11 de BoxingOracle.sol définit quelque chose appelé un mappage :
mapping(bytes32 => uint) matchIdToIndex;
Un mappage dans Solidity est un type de données spécial pour les recherches rapides ; essentiellement une table de recherche ou similaire à une table de hachage, dans laquelle les données contenues vivent sur la blockchain elle-même (lorsque le mappage est défini, comme il l'est ici, en tant que membre de classe). Au cours de l'exécution du contrat, nous pouvons ajouter des données au mappage, comme si vous ajoutiez des données à une table de hachage, et rechercher ultérieurement les valeurs que nous avons ajoutées. Notez à nouveau que dans ce cas, les données que nous ajoutons sont ajoutées à la blockchain elle-même, donc elles persisteront. Si nous l'ajoutons à la cartographie aujourd'hui à New York, dans une semaine quelqu'un à Istanbul pourra le lire.
Exemple d'ajout au mapping, à partir de la ligne 71 de BoxingOracle.sol :
matchIdToIndex[id] = newIndex+1
Exemple de lecture depuis le mapping, depuis la ligne 51 de BoxingOracle.sol :
uint index = matchIdToIndex[_matchId];
Les éléments peuvent également être supprimés du mappage. Il n'est pas utilisé dans ce projet, mais il ressemblerait à ceci :
delete matchIdToIndex[_matchId];
Valeurs de retour
Comme vous l'avez peut-être remarqué, Solidity peut avoir une ressemblance superficielle avec Javascript, mais il n'hérite pas beaucoup du relâchement des types et des définitions de JavaScript. Un code de contrat doit être défini de manière assez stricte et restreinte (et c'est probablement une bonne chose, compte tenu du cas d'utilisation). Dans cet esprit, considérez la définition de fonction de la ligne 40 de BoxingOracle.sol
function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }
OK, alors, commençons par faire un bref aperçu de ce qui est contenu ici. function
le marque comme une fonction. _getMatchIndex
est le nom de la fonction (le trait de soulignement est une convention qui indique un membre privé - nous en discuterons plus tard). Il prend un argument, nommé _matchId
(cette fois la convention de soulignement est utilisée pour désigner les arguments de la fonction) de type bytes32
. Le mot-clé private
rend en fait le membre privé dans sa portée, view
indique au compilateur que cette fonction ne modifie aucune donnée sur la blockchain, et enfin : ~~~ la solidité renvoie (uint) ~~~
Cela signifie que la fonction renvoie un uint (une fonction qui renvoie void n'aurait simplement pas de clause de returns
ici). Pourquoi uint est-il entre parenthèses ? C'est parce que les fonctions Solidity peuvent retourner et retournent souvent des tuples .
Considérons maintenant la définition suivante de la ligne 166 :
function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner) { ... }
Consultez la clause de retour sur celui-ci! Il renvoie une, deux… sept choses différentes. OK, donc, cette fonction renvoie ces choses sous forme de tuple. Pourquoi? Au cours du développement, vous aurez souvent besoin de renvoyer une structure (s'il s'agissait de JavaScript, vous voudriez probablement renvoyer un objet JSON). Eh bien, au moment d'écrire ces lignes (bien que cela puisse changer à l'avenir), Solidity ne prend pas en charge les structures de retour des fonctions publiques. Vous devez donc renvoyer des tuples à la place. Si vous êtes un gars Python, vous êtes peut-être déjà à l'aise avec les tuples. Cependant, de nombreuses langues ne les prennent pas vraiment en charge, du moins pas de cette manière.
Voir ligne 159 pour un exemple de retour d'un tuple comme valeur de retour :
return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);
Et comment acceptons-nous la valeur de retour de quelque chose comme ça ? On peut faire comme ça :
var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
Alternativement, vous pouvez déclarer les variables explicitement au préalable, avec leurs types corrects :
//declare the variables bytes32 id; string name; ... etc... int8 winner; //assign their values (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
Et maintenant, nous avons déclaré 7 variables pour contenir les 7 valeurs de retour, que nous pouvons maintenant utiliser. Sinon, en supposant que nous ne voulions qu'une ou deux des valeurs, nous pouvons dire :
//declare the variables bytes32 id; uint date; //assign their values (id,,,,date,,) = getMostRecentMatch(false);
Vous voyez ce que nous avons fait là-bas ? Nous avons juste les deux qui nous intéressaient. Regardez toutes ces virgules. Il faut bien les compter !
Importations
Les lignes 3 et 4 de BoxingOracle.sol sont des importations :
import "./Ownable.sol"; import "./DateLib.sol";
Comme vous vous en doutez probablement, il s'agit d'importer des définitions à partir de fichiers de code qui existent dans le même dossier de projet de contrats que BoxingOracle.sol.
Modificateurs
Notez que les définitions de fonction ont un tas de modificateurs attachés. Tout d'abord, il y a la visibilité : visibilité privée, publique, interne et externe de la fonction.
De plus, vous verrez les mots-clés pure
et view
. Ceux-ci indiquent au compilateur le type de modifications que la fonction apportera, le cas échéant. Ceci est important car une telle chose est un facteur dans le coût final du gaz pour faire fonctionner la fonction. Voir ici pour l'explication : Solidity Docs.
Enfin, ce dont je veux vraiment discuter, ce sont les modificateurs personnalisés. Jetez un oeil à la ligne 61 de BoxingOracle.sol :
function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {
Notez le modificateur onlyOwner
juste avant le mot-clé « public ». Cela indique que seul le propriétaire du contrat peut appeler cette méthode ! Bien que très important, ce n'est pas une fonctionnalité native de Solidity (bien que ce le sera peut-être à l'avenir). En fait, onlyOwner
est un exemple de modificateur personnalisé que nous créons nous-mêmes et utilisons. Regardons.
Tout d'abord, le modificateur est défini dans le fichier Ownable.sol, que vous pouvez voir que nous avons importé à la ligne 3 de BoxingOracle.sol :
import "./Ownable.sol"
Notez que, pour utiliser le modificateur, nous avons fait hériter BoxingOracle
de Ownable
. À l'intérieur de Ownable.sol, à la ligne 25, nous pouvons trouver la définition du modificateur à l'intérieur du contrat "Ownable":
modifier onlyOwner() { require(msg.sender == owner); _; }
(Ce contrat Ownable, soit dit en passant, est tiré de l'un des contrats publics d'OpenZeppelin.)
Notez que cette chose est déclarée comme un modificateur, indiquant que nous pouvons l'utiliser tel quel, pour modifier une fonction. Notez que la viande du modificateur est une déclaration "require". Les instructions Require sont un peu comme les assertions, mais pas pour le débogage. Si la condition de l'instruction require échoue, la fonction lèvera une exception. Donc, pour paraphraser cette déclaration "require":
require(msg.sender == owner);
On pourrait dire que cela signifie :
if (msg.send != owner) throw an exception;
Et, en fait, dans Solidity 0.4.22 et supérieur, nous pouvons ajouter un message d'erreur à cette instruction require :
require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only");
Enfin, dans la ligne curieuse:
_;
Le trait de soulignement est un raccourci pour "Ici, exécutez le contenu complet de la fonction modifiée". Ainsi, en effet, l'instruction require sera exécutée en premier, suivie de la fonction réelle. C'est donc comme si cette ligne de logique était en attente avant la fonction modifiée.

Il y a, bien sûr, plus de choses que vous pouvez faire avec les modificateurs. Vérifiez les documents : Docs.
Bibliothèques Solidité
Il existe une fonctionnalité de langage de Solidity connue sous le nom de bibliothèque . Nous avons un exemple dans notre projet sur DateLib.sol.
Il s'agit d'une bibliothèque pour une meilleure gestion plus facile des types de date. Il est importé dans BoxingOracle à la ligne 4 :
import "./DateLib.sol";
Et il est utilisé à la ligne 13 :
using DateLib for DateLib.DateTime;
DateLib.DateTime
est une structure qui est exposée à partir du contrat DateLib (elle est exposée en tant que membre ; voir la ligne 4 de DateLib.sol) et nous déclarons ici que nous « utilisons » la bibliothèque DateLib pour un certain type de données. Ainsi, les méthodes et les opérations déclarées dans cette bibliothèque s'appliqueront au type de données que nous avons défini. C'est ainsi qu'une bibliothèque est utilisée dans Solidity.
Pour un exemple plus clair, consultez certaines des bibliothèques d'OpenZeppelin pour les nombres, telles que SafeMath. Ceux-ci peuvent être appliqués aux types de données Solidity natifs (numériques) (alors que nous avons appliqué ici une bibliothèque à un type de données personnalisé) et sont largement utilisés.
Interfaces
Comme dans les langages orientés objet traditionnels, les interfaces sont prises en charge. Les interfaces dans Solidity sont définies comme des contrats, mais les corps de fonction sont omis pour les fonctions. Pour obtenir un exemple de définition d'interface, consultez OracleInterface.sol. Dans cet exemple, l'interface est utilisée comme remplaçant du contrat oracle, dont le contenu réside dans un contrat distinct avec une adresse distincte.
Conventions de nommage
Bien sûr, les conventions de nommage ne sont pas une règle globale ; en tant que programmeurs, nous savons que nous sommes libres de suivre les conventions de codage et de nommage qui nous intéressent. D'un autre côté, nous voulons que les autres se sentent à l'aise pour lire et travailler avec notre code, donc un certain degré de standardisation est souhaitable.
Aperçu du projet
Maintenant que nous avons passé en revue certaines fonctionnalités générales du langage présentes dans les fichiers de code en question, nous pouvons commencer à examiner plus spécifiquement le code lui-même, pour ce projet.
Alors, clarifions le but de ce projet, encore une fois. Le but de ce projet est de fournir une démonstration semi-réaliste (ou pseudo-réaliste) et un exemple d'un contrat intelligent qui utilise un oracle. Au fond, il s'agit simplement d'un contrat faisant appel à un autre contrat distinct.
L'analyse de rentabilisation de l'exemple peut être énoncée comme suit :
- Un utilisateur veut faire des paris de différentes tailles sur des matchs de boxe, payer de l'argent (ether) pour les paris et collecter ses gains quand et s'il gagne.
- Un utilisateur fait ces paris via un contrat intelligent. (Dans un cas d'utilisation réel, il s'agirait d'un DApp complet avec une interface Web3 ; mais nous n'examinons que le côté des contrats.)
- Un contrat intelligent distinct, l'oracle, est géré par un tiers. Son travail consiste à maintenir une liste des matchs de boxe avec leurs états actuels (en attente, en cours, terminé, etc.) et, s'ils sont terminés, le vainqueur.
- Le contrat principal obtient des listes de matchs en attente de l'oracle et les présente aux utilisateurs comme des matchs «pariables».
- Le contrat principal accepte les paris jusqu'au début d'un match.
- Une fois qu'un match est décidé, le contrat principal répartit les gains et les pertes selon un algorithme simple, prend une coupe pour lui-même et verse les gains sur demande (les perdants perdent simplement la totalité de leur mise).
Les règles de paris :
- Il y a un pari minimum défini (défini en wei).
- Il n'y a pas de pari maximum ; les utilisateurs peuvent parier n'importe quel montant qu'ils aiment au-dessus du minimum.
- Les utilisateurs peuvent placer des paris jusqu'à ce que le match devienne "en cours".
Algorithme de partage des gains :
- Tous les paris reçus sont placés dans un « pot ».
- Un petit pourcentage est retiré du pot, pour la maison.
- Chaque gagnant reçoit une part du pot, directement proportionnelle à la taille relative de ses paris.
- Les gains sont calculés dès que le tout premier utilisateur demande les résultats, une fois le match décidé.
- Les gains sont attribués sur demande de l'utilisateur.
- En cas d'égalité, personne ne gagne - tout le monde récupère sa mise et la maison ne prend aucune coupe.
BoxingOracle : le contrat Oracle
Principales fonctions fournies
L'oracle a deux interfaces, pourrait-on dire : une présentée au « propriétaire » et mainteneur du contrat et une présentée au grand public ; c'est-à-dire des contrats qui consomment l'oracle. Le mainteneur, il offre une fonctionnalité pour alimenter les données dans le contrat, en prenant essentiellement des données du monde extérieur et en les mettant sur la blockchain. Au public, il offre un accès en lecture seule à ces données. Il est important de noter que le contrat lui-même interdit aux non-propriétaires de modifier les données, mais l'accès en lecture seule à ces données est accordé publiquement sans restriction.
Aux utilisateurs :
- Lister tous les matchs
- Lister les matchs en attente
- Obtenir les détails d'un match spécifique
- Obtenir le statut et le résultat d'un match spécifique
Au propriétaire :
- Saisissez une correspondance
- Changer le statut du match
- Définir le résultat du match
Histoire de l'utilisateur:
- Un nouveau match de boxe est annoncé et confirmé pour le 9 mai.
- Moi, le responsable du contrat (peut-être suis-je un réseau sportif bien connu ou un nouveau point de vente), j'ajoute le match à venir aux données de l'oracle sur la blockchain, avec le statut "en attente". Toute personne ou tout contrat peut désormais interroger et utiliser ces données comme bon lui semble.
- Lorsque le match commence, je définis le statut de ce match sur "en cours".
- Lorsque le match se termine, je règle le statut du match sur "terminé" et modifie les données du match pour désigner le gagnant.
Révision du code Oracle
Cette revue est entièrement basée sur BoxingOracle.sol; les numéros de ligne font référence à ce fichier.
Sur les lignes 10 et 11, nous déclarons notre lieu de stockage des allumettes :
Match[] matches; mapping(bytes32 => uint) matchIdToIndex;
matches
n'est qu'un simple tableau pour stocker les instances de correspondance, et le mappage n'est qu'une fonction permettant de mapper un ID de correspondance unique (une valeur bytes32) à son index dans le tableau afin que si quelqu'un nous donne un ID brut d'une correspondance, nous pouvons utilisez ce mappage pour le localiser.
À la ligne 17, notre structure de correspondance est définie et expliquée :
//defines a match along with its outcome struct Match { bytes32 id; //unique id string name; //human-friendly name (eg, Jones vs. Holloway) string participants; //a delimited string of participant names uint8 participantCount; //number of participants (always 2 for boxing matches!) uint date; //GMT timestamp of date of contest MatchOutcome outcome; //the outcome (if decided) int8 winner; //index of the participant who is the winner } //possible match outcomes enum MatchOutcome { Pending, //match has not been fought to decision Underway, //match has started & is underway Draw, //anything other than a clear winner (eg, cancelled) Decided //index of participant who is the winner }
Ligne 61 : La fonction addMatch
est réservée à l'usage du propriétaire du contrat ; il permet d'ajouter une nouvelle correspondance aux données stockées.
Ligne 80 : La fonction declareOutcome
permet au propriétaire du contrat de définir une correspondance comme « décidée », en définissant le participant qui a gagné.
Lignes 102-166 : Les fonctions suivantes sont toutes appelables par le public. Il s'agit des données en lecture seule ouvertes au public en général :
- La fonction
getPendingMatches
renvoie une liste d'ID de toutes les correspondances dont l'état actuel est "en attente". - La fonction
getAllMatches
renvoie une liste des ID de toutes les correspondances. - La fonction
getMatch
renvoie les détails complets d'une seule correspondance, spécifiée par ID.
Les lignes 193-204 déclarent des fonctions qui sont principalement destinées aux tests, au débogage et aux diagnostics.
- La fonction
testConnection
teste simplement que nous sommes capables d'appeler le contrat. - La fonction
getAddress
renvoie l'adresse de ce contrat. - La fonction
addTestData
ajoute un tas de correspondances de test à la liste des correspondances.
N'hésitez pas à explorer un peu le code avant de passer aux étapes suivantes. Je suggère d'exécuter à nouveau le contrat oracle en mode débogage (comme décrit dans la partie 1 de cette série), d'appeler différentes fonctions et d'examiner les résultats.
BoxingBets : le contrat client
Il est important de définir ce dont le contrat client (le contrat de pari) est responsable et ce dont il n'est pas responsable. Le contrat client n'est pas responsable de la tenue de listes de vrais matchs de boxe ni de la déclaration de leurs résultats. Nous « faisons confiance » (oui, je sais, il y a ce mot sensible – euh oh – nous en discuterons dans la partie 3) à l'oracle de ce service. Le contrat client est responsable de l'acceptation des paris. Il est responsable de l'algorithme qui répartit les gains et les transfère sur les comptes des gagnants en fonction du résultat du match (tel que reçu de l'oracle).
De plus, tout est basé sur le pull et il n'y a pas d'événements ou de push. Le contrat extrait les données de l'oracle. Le contrat extrait le résultat du match de l'oracle (en réponse à la demande de l'utilisateur) et le contrat calcule les gains et les transfère en réponse à la demande de l'utilisateur.
Principales fonctions fournies
- Lister tous les matchs en attente
- Obtenir les détails d'un match spécifique
- Obtenir le statut et le résultat d'un match spécifique
- Placer un pari
- Demander/recevoir des gains
Examen du code client
Cette revue est entièrement basée sur BoxingBets.sol; les numéros de ligne font référence à ce fichier.
Les lignes 12 et 13, les premières lignes de code du contrat, définissent des mappages dans lesquels nous stockerons les données de notre contrat.
La ligne 12 mappe les adresses des utilisateurs sur des listes d'ID. Il s'agit de mapper un utilisateur à une liste d'ID de paris qui appartiennent à l'utilisateur. Ainsi, pour une adresse d'utilisateur donnée, nous pouvons rapidement obtenir une liste de tous les paris qui ont été effectués par cet utilisateur.
mapping(address => bytes32[]) private userToBets;
La ligne 13 fait correspondre l'identifiant unique d'un match à une liste d'instances de pari. Avec cela, nous pouvons, pour un match donné, obtenir une liste de tous les paris qui ont été faits pour ce match.
mapping(bytes32 => Bet[]) private matchToBets;
Les lignes 17 et 18 sont liées à la connexion à notre oracle. Tout d'abord, dans la variable boxingOracleAddr
, nous stockons l'adresse du contrat oracle (mis à zéro par défaut). Nous pourrions coder en dur l'adresse de l'oracle, mais nous ne pourrions jamais la changer. (Ne pas pouvoir changer l'adresse de l'oracle peut être une bonne ou une mauvaise chose - nous pouvons en discuter dans la partie 3). La ligne suivante crée une instance de l'interface de l'oracle (qui est définie dans OracleInterface.sol) et la stocke dans une variable.
//boxing results oracle address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);
Si vous passez directement à la ligne 58, vous verrez la fonction setOracleAddress
, dans laquelle cette adresse oracle peut être modifiée et dans laquelle l'instance boxingOracle
est réinstanciée avec une nouvelle adresse.
La ligne 21 définit notre taille de pari minimum, en wei. Il s'agit bien sûr en fait d'une très petite quantité, juste 0,000001 éther.
uint internal minimumBet = 1000000000000;
Aux lignes 58 et 66 respectivement, nous avons les setOracleAddress
et getOracleAddress
. Le setOracleAddress
a le modificateur onlyOwner
car seul le propriétaire du contrat peut remplacer l'oracle par un autre oracle (ce n'est probablement pas une bonne idée, mais nous développerons dans la partie 3). La fonction getOracleAddress
, en revanche, est publiquement appelable ; n'importe qui peut voir quel oracle est utilisé.
function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {... function getOracleAddress() external view returns (address) { ....
Aux lignes 72 et 79, nous avons respectivement les fonctions getBettableMatches
et getMatch
. Notez qu'il s'agit simplement de transférer les appels vers l'oracle et de renvoyer le résultat.
function getBettableMatches() public view returns (bytes32[]) {... function getMatch(bytes32 _matchId) public view returns ( ....
La fonction placeBet
est très importante (ligne 108).
function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...
Une caractéristique frappante de celui-ci est le modificateur payable
; nous avons été tellement occupés à discuter des fonctionnalités générales du langage que nous n'avons pas encore abordé la fonctionnalité essentielle qui consiste à pouvoir envoyer de l'argent avec des appels de fonction ! C'est essentiellement ce dont il s'agit - c'est une fonction qui peut accepter une somme d'argent avec tout autre argument et donnée envoyée.
Nous en avons besoin ici car c'est là que l'utilisateur définit simultanément quel pari il va faire, combien d'argent il a l'intention d'avoir sur ce pari et envoie réellement l'argent. Le modificateur payable
permet cela. Avant d'accepter le pari, nous faisons un tas de vérifications pour nous assurer de la validité du pari. Le premier chèque à la ligne 111 est :
require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");
Le montant d'argent envoyé est stocké dans msg.value
. En supposant que tous les chèques passent, à la ligne 123, nous transférerons ce montant dans la propriété de l'oracle, retirant la propriété de ce montant à l'utilisateur et en possession du contrat :
address(this).transfer(msg.value);
Enfin, à la ligne 136, nous avons une fonction d'assistance de test/débogage qui nous aidera à savoir si le contrat est connecté ou non à un oracle valide :
function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }
Emballer
Et c'est en fait aussi loin que cet exemple va; juste accepter le pari. La fonctionnalité de répartition des gains et de paiement, ainsi que d'autres logiques, ont été intentionnellement laissées de côté afin de garder l'exemple suffisamment simple pour notre objectif, qui est simplement de démontrer l'utilisation d'un oracle avec un contrat. Cette logique plus complète et complexe existe actuellement dans un autre projet, qui est une extension de cet exemple et est toujours en développement.
Nous avons donc maintenant une meilleure compréhension de la base de code et l'avons utilisée comme véhicule et point de départ pour discuter de certaines des fonctionnalités linguistiques offertes par Solidity. L'objectif principal de cette série en trois parties est de démontrer et de discuter de l'utilisation d'un contrat avec un oracle. Le but de cette partie est de comprendre un peu mieux ce code spécifique et de l'utiliser comme point d'embarquement pour comprendre certaines fonctionnalités de Solidity et du développement de contrats intelligents. Le but de la troisième et dernière partie sera de discuter de la stratégie et de la philosophie de l'utilisation d'Oracle et de la façon dont elle s'intègre conceptuellement dans le modèle de contrat intelligent.
Autres étapes facultatives
J'encourage vivement les lecteurs qui souhaitent en savoir plus à prendre ce code et à jouer avec. Implémenter de nouvelles fonctionnalités. Corrigez les bogues. Implémenter des fonctionnalités non implémentées (telles que l'interface de paiement). Testez les appels de fonction. Modifiez-les et retestez pour voir ce qui se passe. Ajoutez un frontal web3. Ajoutez une fonction permettant de supprimer des matchs ou de modifier leurs résultats (en cas d'erreur). Et les matchs annulés ? Implémenter un deuxième oracle. Bien sûr, un contrat est libre d'utiliser autant d'oracles qu'il le souhaite, mais quels problèmes cela entraîne-t-il ? Aie du plaisir avec ça; c'est une excellente façon d'apprendre, et quand vous le faites de cette façon (et que vous en tirez du plaisir), vous êtes sûr de retenir plus de ce que vous avez appris.
Un exemple de liste non exhaustive de choses à essayer :
- Exécutez à la fois le contrat et l'oracle dans le testnet local (dans la truffe, comme décrit dans la partie 1) et appelez toutes les fonctions appelables et toutes les fonctions de test.
- Ajoutez une fonctionnalité pour calculer les gains et les payer à la fin d'un match.
- Ajouter une fonctionnalité pour rembourser tous les paris en cas de match nul.
- Ajoutez une fonctionnalité pour demander un remboursement ou annuler un pari, avant le début du match.
- Ajoutez une fonctionnalité pour tenir compte du fait que les matchs peuvent parfois être annulés (auquel cas tout le monde aura besoin d'un remboursement).
- Implémentez une fonctionnalité pour garantir que l'oracle qui était en place lorsqu'un utilisateur a placé un pari est le même oracle qui sera utilisé pour déterminer le résultat de ce match.
- Implémentez un autre (deuxième) oracle, qui a des caractéristiques différentes qui lui sont associées, ou sert éventuellement un sport autre que la boxe (notez que les participants comptent et que la liste permet différents types de sports, nous ne sommes donc pas limités à la boxe) .
- Implémentez
getMostRecentMatch
afin qu'il renvoie soit la correspondance la plus récemment ajoutée, soit la correspondance la plus proche de la date actuelle en termes de moment où elle se produira. - Implémenter la gestion des exceptions.
Une fois que vous serez familiarisé avec les mécanismes de la relation entre le contrat et l'oracle, dans la partie 3 de cette série en trois parties, nous aborderons certaines des questions stratégiques, de conception et philosophiques soulevées par cet exemple.