Ethereum Oracle Contracts: Solidity-Code-Funktionen
Veröffentlicht: 2022-03-11Im ersten Abschnitt dieses Dreiteilers haben wir ein kleines Tutorial durchgearbeitet, das uns ein einfaches Vertrag-mit-Orakel-Paar gegeben hat. Die Mechanismen und Prozesse zum Einrichten (mit Trüffel), Kompilieren des Codes, Bereitstellen in einem Testnetzwerk, Ausführen und Debuggen wurden beschrieben; Viele Details des Codes wurden jedoch handgewellt beschönigt. Also, wie versprochen, werden wir uns nun einige dieser Sprachfunktionen ansehen, die einzigartig für die intelligente Vertragsentwicklung von Solidity und einzigartig für dieses spezielle Vertrags-Orakel-Szenario sind. Wir können uns zwar nicht jedes einzelne Detail genau ansehen (das überlasse ich Ihnen, wenn Sie möchten, in Ihren weiteren Studien), aber wir werden versuchen, die auffälligsten, interessantesten und wichtigsten Merkmale des Codes zu finden.
Um dies zu erleichtern, empfehle ich, dass Sie entweder Ihre eigene Version des Projekts öffnen (falls Sie eine haben) oder den Code als Referenz zur Hand haben.
Den vollständigen Code an dieser Stelle finden Sie hier: https://github.com/jrkosinski/oracle-example/tree/part2-step1
Ethereum und Solidität
Solidity ist nicht die einzige verfügbare Sprache für die Entwicklung intelligenter Verträge, aber ich denke, es ist sicher genug zu sagen, dass es die gebräuchlichste und beliebteste im Allgemeinen für intelligente Verträge von Ethereum ist. Zum Zeitpunkt des Schreibens dieses Artikels ist es sicherlich derjenige, der die beliebteste Unterstützung und Informationen bietet.
Solidity ist objektorientiert und Turing-vollständig. Allerdings werden Sie schnell die eingebauten (und völlig beabsichtigten) Einschränkungen erkennen, die dazu führen, dass sich die intelligente Vertragsprogrammierung ganz anders anfühlt als das gewöhnliche Let's-do-this-thing-Hacking.
Solidity-Version
Hier ist die erste Zeile jedes Solidity-Codegedichts:
pragma solidity ^0.4.17;
Die Versionsnummern, die Sie sehen, werden unterschiedlich sein, da sich Solidity, das noch in den Kinderschuhen steckt, schnell verändert und weiterentwickelt. Version 0.4.17 ist die Version, die ich in meinen Beispielen verwendet habe; die neueste Version zum Zeitpunkt dieser Veröffentlichung ist 0.4.25.
Die neueste Version zu diesem Zeitpunkt, an dem Sie dies lesen, kann durchaus etwas ganz anderes sein. Viele nette Features sind in Arbeit (oder zumindest geplant) für Solidity, die wir gleich besprechen werden.
Hier finden Sie eine Übersicht über verschiedene Solidity-Versionen.
Profi-Tipp: Sie können auch eine Reihe von Versionen angeben (obwohl ich dies nicht allzu oft sehe), wie folgt:
pragma solidity >=0.4.16 <0.6.0;
Funktionen der Solidity-Programmiersprache
Solidity hat viele Sprachmerkmale, die den meisten modernen Programmierern vertraut sind, sowie einige, die unterschiedlich und (zumindest für mich) ungewöhnlich sind. Es soll von C++, Python und JavaScript inspiriert worden sein – die mir persönlich alle sehr vertraut sind, und doch scheint sich Solidity deutlich von all diesen Sprachen zu unterscheiden.
Vertrag
Die .sol-Datei ist die grundlegende Codeeinheit. Beachten Sie in BoxingOracle.sol die 9. Zeile:
contract BoxingOracle is Ownable {
Da die Klasse die Grundeinheit der Logik in objektorientierten Sprachen ist, ist der Vertrag die Grundeinheit der Logik in Solidity. Zur Vereinfachung genügt es vorerst zu sagen, dass der Vertrag die „Klasse“ von Solidity ist (für objektorientierte Programmierer ist dies ein einfacher Sprung).
Nachlass
Solidity-Verträge unterstützen die Vererbung vollständig, und sie funktioniert wie erwartet; private Vertragsmitglieder werden nicht vererbt, geschützte und öffentliche hingegen schon. Überladen und Polymorphismus werden erwartungsgemäß unterstützt.
contract BoxingOracle is Ownable {
In der obigen Anweisung bezeichnet das Schlüsselwort „is“ die Vererbung. BoxingOracle erbt von Ownable. Mehrfachvererbung wird auch in Solidity unterstützt. Mehrfachvererbung wird durch eine durch Kommas getrennte Liste von Klassennamen angezeigt, etwa so:
contract Child is ParentA, ParentB, ParentC { …
Während es (meiner Meinung nach) keine gute Idee ist, bei der Strukturierung Ihres Vererbungsmodells übermäßig kompliziert zu werden, finden Sie hier einen interessanten Artikel über Solidität in Bezug auf das sogenannte Diamantproblem.
Aufzählungen
Aufzählungen werden in Solidity unterstützt:
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 }
Wie zu erwarten (nicht anders als in bekannten Sprachen), wird jedem Aufzählungswert ein ganzzahliger Wert zugewiesen, beginnend mit 0. Wie in der Solidity-Dokumentation angegeben, sind Aufzählungswerte in alle ganzzahligen Typen konvertierbar (z. etc.), aber eine implizite Konvertierung ist nicht erlaubt. Das bedeutet, dass sie explizit gecastet werden müssen (z. B. in uint).
Solidity Docs: Enums Enums-Lernprogramm
Strukturen
Strukturen sind eine weitere Möglichkeit, wie Aufzählungen, einen benutzerdefinierten Datentyp zu erstellen. Structs sind allen C/C++ Foundation Codern und alten Leuten wie mir vertraut. Ein Beispiel für eine Struktur aus Zeile 17 von 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; }
Hinweis für alle alten C-Programmierer: Das „Packen“ von Strukturen in Solidity ist eine Sache, aber es gibt einige Regeln und Vorbehalte. Gehen Sie nicht unbedingt davon aus, dass es genauso funktioniert wie in C; Überprüfen Sie die Dokumente und seien Sie sich Ihrer Situation bewusst, um festzustellen, ob das Packen Ihnen in einem bestimmten Fall helfen wird oder nicht.
Festigkeitsstruktur Verpackung
Einmal erstellte Strukturen können in Ihrem Code als native Datentypen angesprochen werden. Hier ist ein Beispiel für die Syntax für die „Instanziierung“ des oben erstellten Strukturtyps:
Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1);
Datentypen in Solidität
Dies bringt uns zum sehr grundlegenden Thema der Datentypen in Solidity. Welche Datentypen unterstützt solidity? Solidity ist statisch typisiert, und zum Zeitpunkt des Schreibens dieses Artikels müssen Datentypen explizit deklariert und an Variablen gebunden werden.
Soliditätsdatentypen
Boolesche Werte
Boolesche Typen werden unter dem Namen bool und den Werten true oder false unterstützt
Numerische Typen
Ganzzahltypen werden unterstützt, sowohl mit Vorzeichen als auch ohne Vorzeichen, von int8/uint8 bis int256/uint256 (das sind 8-Bit-Ganzzahlen bis 256-Bit-Ganzzahlen). Der Typ uint ist die Abkürzung für uint256 (und ebenso ist int die Abkürzung für int256).
Insbesondere werden Fließkommatypen nicht unterstützt. Warum nicht? Nun, zum einen sind Fließkommavariablen im Umgang mit Geldwerten bekanntlich eine schlechte Idee (allgemein natürlich), weil sich der Wert in Luft auflösen kann. Ätherwerte werden in Wei angegeben, was 1/1.000.000.000.000.000.000stel eines Äthers ist, und das muss für alle Zwecke ausreichend genau sein; Sie können einen Äther nicht in kleinere Teile zerlegen.
Festkommawerte werden derzeit teilweise unterstützt. Laut Solidity-Dokumentation: „Festkommazahlen werden noch nicht vollständig von Solidity unterstützt. Sie können deklariert, aber nicht zugeordnet werden.“
https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9
Hinweis: In den meisten Fällen ist es am besten, einfach uint zu verwenden, da das Verringern der Größe der Variablen (z. B. auf uint32) die Gaskosten tatsächlich erhöhen kann, anstatt sie wie erwartet zu senken. Verwenden Sie als allgemeine Faustregel uint, es sei denn, Sie sind sich sicher, dass Sie einen guten Grund dafür haben.
String-Typen
Der String-Datentyp in Solidity ist ein lustiges Thema; Je nachdem, mit wem Sie sprechen, können Sie unterschiedliche Meinungen erhalten. Es gibt einen String-Datentyp in Solidity, das ist eine Tatsache. Meine Meinung, die wahrscheinlich von den meisten geteilt wird, ist, dass es nicht viel Funktionalität bietet. String-Parsing, -Verkettung, -Ersetzung, -Trimmen, sogar das Zählen der Länge des Strings: Keines dieser Dinge, die Sie wahrscheinlich von einem String-Typ erwarten, ist vorhanden, und daher liegen sie in Ihrer Verantwortung (falls Sie sie brauchen). Einige Leute verwenden bytes32 anstelle von string; das kann man auch machen.
Lustiger Artikel über Solidity-Saiten
Meine Meinung: Es könnte eine lustige Übung sein, einen eigenen String-Typ zu schreiben und ihn für den allgemeinen Gebrauch zu veröffentlichen.
Adresstyp
Vielleicht einzigartig bei Solidity haben wir einen Adressdatentyp , speziell für Ethereum-Wallet- oder Vertragsadressen. Es ist ein 20-Byte-Wert speziell zum Speichern von Adressen dieser bestimmten Größe. Außerdem gibt es Typmember speziell für Adressen dieser Art.
address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;
Adressdatentypen
DateTime-Typen
Es gibt in Solidity per se keinen nativen Date- oder DateTime-Typ, wie es beispielsweise in JavaScript der Fall ist. (Oh nein – Solidity klingt mit jedem Absatz schlechter!?) Datumsangaben werden nativ als Zeitstempel vom Typ uint (uint256) adressiert. Sie werden im Allgemeinen als Zeitstempel im Unix-Stil behandelt, in Sekunden und nicht in Millisekunden, da der Block-Zeitstempel ein Zeitstempel im Unix-Stil ist. In Fällen, in denen Sie aus verschiedenen Gründen menschenlesbare Daten benötigen, stehen Open-Source-Bibliotheken zur Verfügung. Sie werden vielleicht bemerken, dass ich eine in BoxingOracle verwendet habe: DateLib.sol. OpenZeppelin hat auch Datums-Utilities sowie viele andere Arten von allgemeinen Utility-Bibliotheken (wir werden uns in Kürze mit der Bibliotheksfunktion von Solidity befassen).
Profi-Tipp: OpenZeppelin ist eine gute Quelle (aber natürlich nicht die einzige gute Quelle) sowohl für Wissen als auch für vorgefertigten generischen Code, der Ihnen beim Aufbau Ihrer Verträge helfen kann.
Zuordnungen
Beachten Sie, dass Zeile 11 von BoxingOracle.sol etwas definiert, das als Mapping bezeichnet wird:
mapping(bytes32 => uint) matchIdToIndex;
Ein Mapping in Solidity ist ein spezieller Datentyp für schnelle Suchen; im Wesentlichen eine Nachschlagetabelle oder ähnlich einer Hashtabelle, wobei die enthaltenen Daten auf der Blockchain selbst leben (wenn die Zuordnung, wie hier, als Klassenmitglied definiert ist). Im Verlauf der Vertragsausführung können wir der Zuordnung Daten hinzufügen, ähnlich wie das Hinzufügen von Daten zu einer Hash-Tabelle, und später die von uns hinzugefügten Werte nachschlagen. Beachten Sie erneut, dass in diesem Fall die von uns hinzugefügten Daten der Blockchain selbst hinzugefügt werden, sodass sie bestehen bleiben. Wenn wir es heute in New York zum Mapping hinzufügen, kann es in einer Woche jemand in Istanbul lesen.
Beispiel für das Hinzufügen zum Mapping aus Zeile 71 von BoxingOracle.sol:
matchIdToIndex[id] = newIndex+1
Beispiel für das Lesen aus dem Mapping, ab Zeile 51 von BoxingOracle.sol:
uint index = matchIdToIndex[_matchId];
Elemente können auch aus der Zuordnung entfernt werden. Es wird in diesem Projekt nicht verwendet, aber es würde so aussehen:
delete matchIdToIndex[_matchId];
Rückgabewerte
Wie Sie vielleicht bemerkt haben, hat Solidity vielleicht eine oberflächliche Ähnlichkeit mit Javascript, aber es erbt nicht viel von JavaScripts lockeren Typen und Definitionen. Ein Vertragscode muss ziemlich streng und eingeschränkt definiert werden (und das ist angesichts des Anwendungsfalls wahrscheinlich eine gute Sache). Betrachten Sie in diesem Sinne die Funktionsdefinition aus Zeile 40 von BoxingOracle.sol
function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }
OK, also lassen Sie uns zunächst einen kurzen Überblick darüber geben, was hier enthalten ist. function
kennzeichnet es als Funktion. _getMatchIndex
ist der Funktionsname (der Unterstrich ist eine Konvention, die ein privates Element angibt – wir werden das später besprechen). Es nimmt ein Argument namens _matchId
(diesmal wird die Konvention des Unterstrichs verwendet, um Funktionsargumente zu bezeichnen) vom Typ bytes32
. Das Schlüsselwort private
macht das Mitglied tatsächlich im Gültigkeitsbereich privat, view
teilt dem Compiler mit, dass diese Funktion keine Daten in der Blockchain ändert, und schließlich: ~~~ solidity return (uint) ~~~
Dies besagt, dass die Funktion ein uint zurückgibt (eine Funktion, die void returns
, hätte hier einfach keine return-Klausel). Warum steht uint in Klammern? Das liegt daran, dass Solidity-Funktionen Tupel zurückgeben können und dies auch oft tun.
Betrachten Sie nun die folgende Definition aus Zeile 166:
function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner) { ... }
Schauen Sie sich die Rückgabeklausel an! Es gibt eins, zwei … sieben verschiedene Dinge zurück. OK, also gibt diese Funktion diese Dinge als Tupel zurück. Warum? Im Laufe der Entwicklung müssen Sie häufig eine Struktur zurückgeben (wenn es JavaScript wäre, würden Sie wahrscheinlich ein JSON-Objekt zurückgeben wollen). Nun, zum jetzigen Zeitpunkt (obwohl sich dies in Zukunft ändern kann) unterstützt Solidity keine Rückgabe von Strukturen aus öffentlichen Funktionen. Sie müssen also stattdessen Tupel zurückgeben. Wenn Sie ein Python-Typ sind, sind Sie vielleicht bereits mit Tupeln vertraut. Viele Sprachen unterstützen sie jedoch nicht wirklich, zumindest nicht auf diese Weise.
In Zeile 159 finden Sie ein Beispiel für die Rückgabe eines Tupels als Rückgabewert:
return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);
Und wie akzeptieren wir den Rückgabewert von so etwas? Wir können so vorgehen:
var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
Alternativ können Sie die Variablen vorher explizit mit ihren richtigen Typen deklarieren:
//declare the variables bytes32 id; string name; ... etc... int8 winner; //assign their values (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
Und jetzt haben wir 7 Variablen deklariert, um die 7 Rückgabewerte zu halten, die wir jetzt verwenden können. Andernfalls können wir unter der Annahme, dass wir nur einen oder zwei der Werte wollten, sagen:
//declare the variables bytes32 id; uint date; //assign their values (id,,,,date,,) = getMostRecentMatch(false);
Sehen Sie, was wir dort gemacht haben? Wir haben nur die beiden, an denen wir interessiert waren. Sehen Sie sich all diese Kommas an. Wir müssen sie sorgfältig zählen!
Importe
Die Zeilen 3 und 4 von BoxingOracle.sol sind Importe:
import "./Ownable.sol"; import "./DateLib.sol";
Wie Sie wahrscheinlich erwarten, importieren diese Definitionen aus Codedateien, die sich im selben Vertragsprojektordner wie BoxingOracle.sol befinden.
Modifikatoren
Beachten Sie, dass die Funktionsdefinitionen eine Reihe von Modifikatoren angehängt haben. Erstens gibt es Sichtbarkeit: privat, öffentlich, intern und extern – Funktionssichtbarkeit.
Außerdem sehen Sie Keywords pure
und view
. Diese geben dem Compiler an, welche Art von Änderungen die Funktion vornehmen wird, falls vorhanden. Dies ist wichtig, weil so etwas ein Faktor in den endgültigen Gaskosten zum Ausführen der Funktion ist. Siehe hier zur Erklärung: Solidity Docs.
Was ich schließlich wirklich diskutieren möchte, sind benutzerdefinierte Modifikatoren. Schauen Sie sich Zeile 61 von BoxingOracle.sol an:
function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {
Beachten Sie den Modifikator onlyOwner
direkt vor dem Schlüsselwort „public“. Dies zeigt an, dass nur der Eigentümer des Vertrags diese Methode aufrufen darf! Obwohl dies sehr wichtig ist, ist dies kein natives Feature von Solidity (obwohl es vielleicht in Zukunft der Fall sein wird). Tatsächlich ist onlyOwner
ein Beispiel für einen benutzerdefinierten Modifikator, den wir selbst erstellen und verwenden. Werfen wir einen Blick.
Zunächst wird der Modifikator in der Datei Ownable.sol definiert, die wir in Zeile 3 von BoxingOracle.sol importiert haben:
import "./Ownable.sol"
Beachten Sie, dass wir, um den Modifikator nutzen zu können, Ownable
dazu gebracht haben, von BoxingOracle
zu erben. Innerhalb von Ownable.sol, in Zeile 25, finden wir die Definition für den Modifikator innerhalb des „Ownable“-Vertrags:
modifier onlyOwner() { require(msg.sender == owner); _; }
(Dieser Ownable-Vertrag stammt übrigens aus einem der öffentlichen Verträge von OpenZeppelin.)
Beachten Sie, dass dieses Ding als Modifikator deklariert ist, was darauf hinweist, dass wir es so verwenden können, wie wir es haben, um eine Funktion zu ändern. Beachten Sie, dass das Kernstück des Modifikators eine „require“-Anweisung ist. Require-Anweisungen sind so etwas wie Asserts, aber nicht zum Debuggen. Wenn die Bedingung der require-Anweisung fehlschlägt, löst die Funktion eine Ausnahme aus. Um diese „erfordern“-Aussage zu paraphrasieren:
require(msg.sender == owner);
Wir könnten sagen, es bedeutet:
if (msg.send != owner) throw an exception;
Und tatsächlich können wir in Solidity 0.4.22 und höher dieser require-Anweisung eine Fehlermeldung hinzufügen:
require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only");
Abschließend in der merkwürdig aussehenden Zeile:
_;
Der Unterstrich ist eine Abkürzung für „Hier den vollständigen Inhalt der geänderten Funktion ausführen“. Tatsächlich wird also zuerst die require-Anweisung ausgeführt, gefolgt von der eigentlichen Funktion. Es ist also so, als würde man diese Logikzeile der modifizierten Funktion voranstellen.
Es gibt natürlich noch mehr Dinge, die Sie mit Modifikatoren tun können. Überprüfen Sie die Dokumente: Dokumente.

Soliditätsbibliotheken
Es gibt eine Sprachfunktion von Solidity, die als Bibliothek bekannt ist. Wir haben ein Beispiel in unserem Projekt bei DateLib.sol.
Dies ist eine Bibliothek zur einfacheren Handhabung von Datumstypen. Es wird in Zeile 4 in BoxingOracle importiert:
import "./DateLib.sol";
Und es wird in Zeile 13 verwendet:
using DateLib for DateLib.DateTime;
DateLib.DateTime
ist eine Struktur, die aus dem DateLib-Vertrag expored wird (es wird als Mitglied bereitgestellt; siehe Zeile 4 von DateLib.sol) und wir erklären hier, dass wir die DateLib-Bibliothek für einen bestimmten Datentyp „verwenden“. Die in dieser Bibliothek deklarierten Methoden und Operationen gelten also für den Datentyp, von dem wir gesagt haben, dass er sollte. So wird eine Bibliothek in Solidity verwendet.
Sehen Sie sich für ein klareres Beispiel einige der OpenZeppelin-Bibliotheken für Zahlen an, z. B. SafeMath. Diese können auf native (numerische) Solidity-Datentypen angewendet werden (wobei wir hier eine Bibliothek auf einen benutzerdefinierten Datentyp angewendet haben) und sind weit verbreitet.
Schnittstellen
Wie in gängigen objektorientierten Sprachen werden Schnittstellen unterstützt. Schnittstellen in Solidity werden als Verträge definiert, aber die Funktionskörper werden für die Funktionen weggelassen. Ein Beispiel für eine Schnittstellendefinition finden Sie unter OracleInterface.sol. In diesem Beispiel wird die Schnittstelle als Stellvertreter für den Oracle-Vertrag verwendet, dessen Inhalt in einem separaten Vertrag mit einer separaten Adresse liegt.
Regeln der Namensgebung
Natürlich sind Namenskonventionen keine globale Regel; Als Programmierer wissen wir, dass es uns frei steht, den Codierungs- und Namenskonventionen zu folgen, die uns gefallen. Andererseits möchten wir, dass andere sich beim Lesen und Arbeiten mit unserem Code wohlfühlen, daher ist ein gewisses Maß an Standardisierung wünschenswert.
Projektübersicht
Nachdem wir nun einige allgemeine Sprachfeatures in den fraglichen Codedateien durchgegangen sind, können wir damit beginnen, den Code selbst für dieses Projekt genauer zu betrachten.
Lassen Sie uns also noch einmal den Zweck dieses Projekts klarstellen. Der Zweck dieses Projekts besteht darin, eine halbrealistische (oder pseudorealistische) Demonstration und ein Beispiel für einen intelligenten Vertrag bereitzustellen, der ein Orakel verwendet. Im Kern ist dies nur ein Vertrag, der einen anderen separaten Vertrag aufruft.
Der Business Case des Beispiels lässt sich wie folgt formulieren:
- Ein Benutzer möchte Wetten unterschiedlicher Größe auf Boxkämpfe abschließen, Geld (Äther) für die Wetten bezahlen und seine Gewinne einziehen, wenn und falls er gewinnt.
- Ein Benutzer schließt diese Wetten über einen Smart Contract ab. (In einem realen Anwendungsfall wäre dies eine vollständige DApp mit einem Web3-Frontend; wir untersuchen jedoch nur die Vertragsseite.)
- Ein separater Smart Contract – das Orakel – wird von einem Dritten gepflegt. Seine Aufgabe ist es, eine Liste von Boxkämpfen mit ihrem aktuellen Status (ausstehend, in Bearbeitung, beendet usw.) und, falls beendet, dem Gewinner zu führen.
- Der Hauptvertrag erhält vom Orakel Listen mit ausstehenden Spielen und präsentiert diese den Benutzern als „bettbare“ Spiele.
- Der Hauptvertrag akzeptiert Wetten bis zum Beginn eines Spiels.
- Nach einem entschiedenen Spiel teilt der Hauptkontrakt nach einem einfachen Algorithmus die Gewinne und Verluste auf, nimmt einen Anteil für sich selbst und zahlt den Gewinn auf Wunsch aus (Verlierer verlieren einfach ihren gesamten Einsatz).
Die Wettregeln:
- Es gibt einen definierten Mindesteinsatz (definiert in Wei).
- Es gibt keinen Höchsteinsatz; Benutzer können jeden beliebigen Betrag über dem Minimum setzen.
- Benutzer können Wetten platzieren, bis das Spiel „in Bearbeitung“ wird.
Algorithmus zur Aufteilung der Gewinne:
- Alle erhaltenen Wetten werden in einen „Pot“ gelegt.
- Ein kleiner Prozentsatz wird für das Haus aus dem Topf genommen.
- Jeder Gewinner erhält einen Teil des Pots, der direkt proportional zur relativen Höhe seiner Einsätze ist.
- Gewinne werden berechnet, sobald der allererste Benutzer die Ergebnisse anfordert, nachdem das Spiel entschieden ist.
- Gewinne werden auf Anfrage des Benutzers vergeben.
- Im Falle eines Unentschiedens gewinnt niemand – jeder bekommt seinen Einsatz zurück und das Haus erhält keinen Anteil.
BoxingOracle: der Oracle-Vertrag
Hauptfunktionen bereitgestellt
Das Orakel hat zwei Schnittstellen, könnte man sagen: eine, die dem „Eigentümer“ und Verwalter des Vertrags präsentiert wird, und eine, die der allgemeinen Öffentlichkeit präsentiert wird; das heißt, Verträge, die das Orakel verbrauchen. Als Betreuer bietet es Funktionen zum Einspeisen von Daten in den Vertrag, wobei im Wesentlichen Daten von der Außenwelt genommen und in die Blockchain gestellt werden. Der Öffentlichkeit bietet es einen Lesezugriff auf diese Daten. Es ist wichtig zu beachten, dass der Vertrag selbst Nichteigentümer daran hindert, Daten zu bearbeiten, aber der Lesezugriff auf diese Daten wird ohne Einschränkung öffentlich gewährt.
An Benutzer:
- Alle Übereinstimmungen auflisten
- Ausstehende Übereinstimmungen auflisten
- Erhalten Sie Details zu einem bestimmten Spiel
- Erhalten Sie den Status und das Ergebnis eines bestimmten Spiels
Zum Besitzer:
- Geben Sie eine Übereinstimmung ein
- Status der Übereinstimmung ändern
- Ergebnis des Spiels festlegen
Benutzer Geschichte:
- Ein neuer Boxkampf wird für den 9. Mai angekündigt und bestätigt.
- Ich, der Betreuer des Vertrags (vielleicht bin ich ein bekannter Sportsender oder eine neue Verkaufsstelle), füge das bevorstehende Spiel mit dem Status „ausstehend“ zu den Daten des Orakels in der Blockchain hinzu. Jeder und jeder Vertrag kann diese Daten nun nach Belieben abfragen und verwenden.
- Wenn das Spiel beginnt, setze ich den Status dieses Spiels auf „in Bearbeitung“.
- Wenn das Spiel endet, setze ich den Status des Spiels auf „abgeschlossen“ und ändere die Spieldaten, um den Gewinner anzugeben.
Oracle-Code-Review
Diese Bewertung basiert vollständig auf BoxingOracle.sol; Zeilennummern verweisen auf diese Datei.
In den Zeilen 10 und 11 deklarieren wir unseren Aufbewahrungsort für Streichhölzer:
Match[] matches; mapping(bytes32 => uint) matchIdToIndex;
matches
ist nur ein einfaches Array zum Speichern von Match-Instanzen, und die Zuordnung ist nur eine Einrichtung zum Zuordnen einer eindeutigen Match-ID (ein bytes32-Wert) zu ihrem Index im Array, sodass wir dies tun können, wenn uns jemand eine Roh-ID einer Übereinstimmung übergibt Verwenden Sie diese Zuordnung, um es zu finden.
In Zeile 17 wird unsere Match-Struktur definiert und erklärt:
//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 }
Zeile 61: Die Funktion addMatch
ist nur zur Verwendung durch den Vertragseigentümer bestimmt; es ermöglicht das Hinzufügen einer neuen Übereinstimmung zu den gespeicherten Daten.
Zeile 80: Die Funktion declareOutcome
ermöglicht es dem Vertragseigentümer, ein Match als „entschieden“ festzulegen und den Teilnehmer festzulegen, der gewonnen hat.
Zeilen 102-166: Die folgenden Funktionen sind alle öffentlich aufrufbar. Dies sind die schreibgeschützten Daten, die im Allgemeinen für die Öffentlichkeit zugänglich sind:
- Die Funktion
getPendingMatches
gibt eine Liste mit IDs aller Übereinstimmungen zurück, deren aktueller Status „ausstehend“ ist. - Die Funktion
getAllMatches
gibt eine Liste der IDs aller Übereinstimmungen zurück. - Die Funktion
getMatch
gibt die vollständigen Details einer einzelnen Übereinstimmung zurück, angegeben durch die ID.
Die Zeilen 193-204 deklarieren Funktionen, die hauptsächlich zum Testen, Debuggen und zur Diagnose dienen.
- Die Funktion
testConnection
testet nur, ob wir den Vertrag aufrufen können. - Die Funktion
getAddress
gibt die Adresse dieses Vertrags zurück. - Die Funktion
addTestData
fügt der Liste der Übereinstimmungen eine Reihe von Testübereinstimmungen hinzu.
Fühlen Sie sich frei, den Code ein wenig zu erkunden, bevor Sie mit den nächsten Schritten fortfahren. Ich schlage vor, den Oracle-Vertrag erneut im Debug-Modus auszuführen (wie in Teil 1 dieser Serie beschrieben), verschiedene Funktionen aufzurufen und die Ergebnisse zu untersuchen.
BoxingBets: Der Kundenvertrag
Es ist wichtig zu definieren, wofür der Kundenvertrag (der Wettvertrag) verantwortlich ist und wofür nicht. Der Kundenvertrag ist nicht dafür verantwortlich, Listen von echten Boxkämpfen zu führen oder deren Ergebnisse bekannt zu geben. Wir „vertrauen“ (ja, ich weiß, es gibt dieses heikle Wort – oh oh – wir werden das in Teil 3 besprechen) dem Orakel für diesen Dienst. Der Kundenvertrag ist für die Annahme von Wetten verantwortlich. Es ist verantwortlich für den Algorithmus, der die Gewinne aufteilt und sie basierend auf dem Ergebnis des Spiels (wie vom Orakel erhalten) auf die Konten der Gewinner überweist.
Außerdem ist alles Pull-basiert und es gibt keine Events oder Pushs. Der Vertrag bezieht Daten aus dem Orakel. Der Vertrag zieht das Ergebnis des Spiels aus dem Orakel (als Antwort auf eine Benutzeranfrage) und der Vertrag berechnet Gewinne und überträgt sie als Antwort auf eine Benutzeranfrage.
Hauptfunktionen bereitgestellt
- Alle ausstehenden Übereinstimmungen auflisten
- Erhalten Sie Details zu einem bestimmten Spiel
- Erhalten Sie den Status und das Ergebnis eines bestimmten Spiels
- Eine Wette abgeben
- Gewinne anfordern/erhalten
Client-Code-Überprüfung
Diese Bewertung basiert vollständig auf BoxingBets.sol; Zeilennummern verweisen auf diese Datei.
Die Zeilen 12 und 13, die ersten Codezeilen im Vertrag, definieren einige Zuordnungen, in denen wir die Daten unseres Vertrags speichern.
Zeile 12 ordnet Benutzeradressen ID-Listen zu. Dies ordnet einen Benutzer einer Liste von IDs von Wetten zu, die dem Benutzer gehören. So können wir für jede beliebige Benutzeradresse schnell eine Liste aller Wetten erhalten, die von diesem Benutzer abgeschlossen wurden.
mapping(address => bytes32[]) private userToBets;
Zeile 13 ordnet die eindeutige ID eines Spiels einer Liste von Wettinstanzen zu. Damit können wir für jedes bestimmte Spiel eine Liste aller Wetten erhalten, die für dieses Spiel abgeschlossen wurden.
mapping(bytes32 => Bet[]) private matchToBets;
Die Zeilen 17 und 18 beziehen sich auf die Verbindung zu unserem Orakel. Zuerst speichern wir in der Variablen boxingOracleAddr
die Adresse des Oracle-Vertrags (standardmäßig auf Null gesetzt). Wir könnten die Adresse des Orakels fest codieren, aber dann könnten wir sie nie ändern. (Die Adresse des Orakels nicht ändern zu können, kann gut oder schlecht sein – wir können das in Teil 3 besprechen). Die nächste Zeile erstellt eine Instanz der Oracle-Schnittstelle (die in OracleInterface.sol definiert ist) und speichert sie in einer Variablen.
//boxing results oracle address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);
Wenn Sie zu Zeile 58 vorspringen, sehen Sie die Funktion setOracleAddress
, in der diese Oracle-Adresse geändert werden kann und in der die boxingOracle
-Instanz mit einer neuen Adresse neu instanziiert wird.
Zeile 21 definiert unsere Mindesteinsatzgröße in Wei. Das ist natürlich eigentlich ein sehr kleiner Betrag, nur 0,000001 Ether.
uint internal minimumBet = 1000000000000;
In Zeile 58 bzw. 66 haben wir die Funktionen setOracleAddress
und getOracleAddress
. Die setOracleAddress
hat den Modifikator onlyOwner
, weil nur der Besitzer des Vertrages das Orakel gegen ein anderes Orakel austauschen kann (wahrscheinlich keine gute Idee, aber wir werden in Teil 3 näher darauf eingehen). Die Funktion getOracleAddress
hingegen ist öffentlich aufrufbar; Jeder kann sehen, welches Orakel verwendet wird.
function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {... function getOracleAddress() external view returns (address) { ....
In den Zeilen 72 und 79 haben wir die Funktionen getBettableMatches
bzw. getMatch
. Beachten Sie, dass diese die Aufrufe einfach an das Orakel weiterleiten und das Ergebnis zurückgeben.
function getBettableMatches() public view returns (bytes32[]) {... function getMatch(bytes32 _matchId) public view returns ( ....
Die placeBet
Funktion ist sehr wichtig (Zeile 108).
function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...
Ein auffälliges Merkmal dieses ist der payable
Modifikator; Wir waren so damit beschäftigt, allgemeine Sprachfunktionen zu diskutieren, dass wir noch nicht auf die zentral wichtige Funktion, Geld zusammen mit Funktionsaufrufen senden zu können, eingegangen sind! Das ist es im Grunde – es ist eine Funktion, die einen Geldbetrag zusammen mit anderen gesendeten Argumenten und Daten akzeptieren kann.
Wir brauchen das hier, weil der Benutzer hier gleichzeitig definiert, welche Wette er abschließen wird, wie viel Geld er mit dieser Wette fahren möchte, und das Geld tatsächlich sendet. Der payable
ermöglicht dies. Bevor wir die Wette annehmen, führen wir eine Reihe von Prüfungen durch, um die Gültigkeit der Wette sicherzustellen. Die erste Prüfung in Zeile 111 lautet:
require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");
Der gesendete Geldbetrag wird in msg.value
gespeichert. Unter der Annahme, dass alle Schecks bestanden werden, übertragen wir diesen Betrag in Zeile 123 in das Eigentum des Orakels, indem wir das Eigentum an diesem Betrag dem Benutzer entziehen und in den Besitz des Vertrags übergehen:
address(this).transfer(msg.value);
Schließlich haben wir in Zeile 136 eine Test-/Debugging-Hilfsfunktion, die uns hilft zu wissen, ob der Vertrag mit einem gültigen Orakel verbunden ist oder nicht:
function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }
Einpacken
Und das ist eigentlich alles, was dieses Beispiel angeht; einfach die Wette annehmen. Auf die Funktionalität der Gewinnaufteilung und -auszahlung sowie einige andere Logiken wurde bewusst verzichtet, um das Beispiel für unseren Zweck einfach genug zu halten, nämlich lediglich die Verwendung eines Orakels mit Vertrag zu demonstrieren. Diese vollständigere und komplexere Logik existiert derzeit in einem anderen Projekt, das eine Erweiterung dieses Beispiels darstellt und sich noch in der Entwicklung befindet.
Jetzt haben wir also ein besseres Verständnis der Codebasis und haben sie als Vehikel und Ausgangspunkt verwendet, um einige der von Solidity angebotenen Sprachfunktionen zu diskutieren. Der Hauptzweck dieser dreiteiligen Serie besteht darin, die Verwendung eines Vertrags mit einem Orakel zu demonstrieren und zu diskutieren. Der Zweck dieses Teils besteht darin, diesen spezifischen Code ein wenig besser zu verstehen und ihn als Ausgangspunkt für das Verständnis einiger Merkmale von Solidity und der Entwicklung intelligenter Verträge zu verwenden. Der Zweck des dritten und letzten Teils besteht darin, die Strategie und Philosophie der Oracle-Nutzung zu diskutieren und wie sie sich konzeptionell in das Smart-Contract-Modell einfügt.
Weitere optionale Schritte
Ich möchte Leser, die mehr erfahren möchten, sehr ermutigen, diesen Code zu nehmen und damit zu spielen. Implementieren Sie neue Funktionen. Beheben Sie alle Fehler. Implementieren Sie nicht implementierte Funktionen (z. B. die Zahlungsschnittstelle). Testen Sie die Funktionsaufrufe. Ändern Sie sie und testen Sie erneut, um zu sehen, was passiert. Fügen Sie ein web3-Frontend hinzu. Fügen Sie eine Möglichkeit hinzu, Übereinstimmungen zu entfernen oder ihre Ergebnisse zu ändern (im Falle eines Fehlers). Was ist mit abgesagten Spielen? Implementieren Sie ein zweites Orakel. Natürlich steht es einem Vertrag frei, so viele Orakel zu verwenden, wie er möchte, aber welche Probleme bringt das mit sich? Viel Spass damit; Das ist eine großartige Art zu lernen, und wenn Sie es auf diese Weise tun (und Spaß daran haben), werden Sie sicher mehr von dem behalten, was Sie gelernt haben.
Eine beispielhafte, nicht vollständige Liste von Dingen, die Sie ausprobieren sollten:
- Führen Sie sowohl den Vertrag als auch das Orakel im lokalen Testnetz aus (in Truffle, wie in Teil 1 beschrieben) und rufen Sie alle aufrufbaren Funktionen und alle Testfunktionen auf.
- Fügen Sie Funktionen zur Berechnung der Gewinne und deren Auszahlung nach Abschluss eines Spiels hinzu.
- Funktionalität zur Rückerstattung aller Wetten im Falle eines Unentschiedens hinzugefügt.
- Fügen Sie eine Funktion hinzu, um eine Rückerstattung zu beantragen oder eine Wette zu stornieren, bevor das Spiel beginnt.
- Fügen Sie eine Funktion hinzu, um zu berücksichtigen, dass Spiele manchmal abgesagt werden können (in diesem Fall benötigen alle eine Rückerstattung).
- Implementieren Sie eine Funktion, um sicherzustellen, dass das Orakel, das vorhanden war, als ein Benutzer eine Wette platzierte, dasselbe Orakel ist, das verwendet wird, um das Ergebnis dieses Spiels zu bestimmen.
- Implementieren Sie ein weiteres (zweites) Orakel, mit dem einige andere Funktionen verbunden sind, oder das möglicherweise einem anderen Sport als dem Boxen dient (beachten Sie, dass die Teilnehmerzählung und -liste verschiedene Sportarten zulässt, sodass wir uns nicht nur auf das Boxen beschränken). .
- Implementieren
getMostRecentMatch
, sodass tatsächlich entweder die zuletzt hinzugefügte Übereinstimmung zurückgegeben wird oder die Übereinstimmung, die dem aktuellen Datum hinsichtlich des Zeitpunkts am nächsten kommt. - Implementieren Sie die Ausnahmebehandlung.
Sobald Sie mit der Mechanik der Beziehung zwischen dem Vertrag und dem Orakel vertraut sind, werden wir in Teil 3 dieser dreiteiligen Serie einige der strategischen, gestalterischen und philosophischen Fragen diskutieren, die durch dieses Beispiel aufgeworfen werden.