以太坊预言机合约:Solidity 代码特性

已发表: 2022-03-11

在这三部分的第一部分中,我们完成了一个小教程,它为我们提供了一个简单的合约与预言机对。 描述了设置(使用 truffle)、编译代码、部署到测试网络、运行和调试的机制和过程; 然而,代码的许多细节都被人为地掩盖了。 所以现在,正如所承诺的,我们将研究一些语言特性,这些特性是 Solidity 智能合约开发所独有的,也是这种特定合约预言机场景所独有的。 虽然我们不能煞费苦心地查看每一个细节(如果您愿意,我将在您的进一步研究中留给您),但我们将尝试找出代码中最引人注目、最有趣和最重要的特性。

为了促进这一点,我建议您打开自己的项目版本(如果有的话),或者将代码放在手边以供参考。

此时的完整代码可以在这里找到:https://github.com/jrkosinski/oracle-example/tree/part2-step1

以太坊和 Solidity

Solidity 并不是唯一可用的智能合约开发语言,但我认为可以肯定地说,它是最常见和最流行的以太坊智能合约。 当然,在撰写本文时,它是拥有最受欢迎的支持和信息的那个。

以太坊 Solidity 关键特性图

Solidity 是面向对象和图灵完备的。 也就是说,你很快就会意识到它的内置(并且完全是有意的)限制,这使得智能合约编程感觉与普通的让我们做这件事的黑客行为完全不同。

Solidity 版本

这是每首 Solidity 代码诗的第一行:

 pragma solidity ^0.4.17;

您看到的版本号会有所不同,因为 Solidity 仍处于年轻阶段,正在迅速变化和发展。 0.4.17 版是我在示例中使用的版本; 本出版物发布时的最新版本是 0.4.25。

此时您正在阅读的最新版本可能完全不同。 Solidity 的许多不错的功能正在开发中(或至少计划中),我们现在将讨论这些功能。

这是不同 Solidity 版本的概述。

专业提示:您还可以指定一系列版本(尽管我不经常看到这样做),如下所示:

 pragma solidity >=0.4.16 <0.6.0;

Solidity 编程语言特性

Solidity 具有大多数现代程序员熟悉的许多语言特性,以及一些独特且(至少对我而言)不寻常的语言特性。 据说它受到了 C++、Python 和 JavaScript 的启发——我个人对所有这些都很熟悉,但 Solidity 似乎与这些语言中的任何一种都截然不同。

合同

.sol 文件是代码的基本单元。 在 BoxingOracle.sol 中,注意第 9 行:

 contract BoxingOracle is Ownable {

由于类是面向对象语言中的基本逻辑单元,因此契约是 Solidity 中的基本逻辑单元。 现在简单地说契约是 Solidity 的“类”就足够了(对于面向对象的程序员来说,这是一个简单的飞跃)。

遗产

Solidity 合约完全支持继承,它可以按你的预期工作; 私人合同成员不被继承,而受保护和公共的成员是。 如您所料,支持重载和多态。

 contract BoxingOracle is Ownable {

在上面的语句中,“is”关键字表示继承。 BoxingOracle 继承自 Ownable。 Solidity 也支持多重继承。 多重继承由一个以逗号分隔的类名列表表示,如下所示:

 contract Child is ParentA, ParentB, ParentC { …

虽然(在我看来)在构建继承模型时过于复杂并不是一个好主意,但这里有一篇关于 Solidity 的关于所谓的钻石问题的有趣文章。

枚举

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 }

正如您所期望的(与熟悉的语言没有什么不同),每个枚举值都被分配一个整数值,从 0 开始。如 Solidity 文档中所述,枚举值可以转换为所有整数类型(例如,uint、uint16、uint32、等),但不允许隐式转换。 这意味着它们必须显式转换(例如,转换为 uint)。

Solidity 文档:枚举枚举教程

结构

结构是另一种创建用户定义数据类型的方式,如枚举。 所有 C/C++ 基础编码人员和像我这样的老家伙都熟悉结构。 BoxingOracle.sol 第 17 行的结构示例:

 //defines a match along with its outcome struct Match { bytes32 id; string name; string participants; uint8 participantCount; uint date; MatchOutcome outcome; int8 winner; }

所有老 C 程序员请注意: Solidity 中的结构“打包”是一回事,但有一些规则和注意事项。 不一定假设它与 C 中的工作方式相同; 检查文档并了解您的情况,以确定在特定情况下包装是否会对您有所帮助。

Solidity 结构封装

创建后,结构可以在您的代码中作为本机数据类型进行寻址。 这是上面创建的结构类型“实例化”的语法示例:

 Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1);

Solidity 中的数据类型

这将我们带到了 Solidity 中数据类型的基本主题。 Solidity 支持哪些数据类型? Solidity 是静态类型的,在撰写本文时,数据类型必须显式声明并绑定到变量。

以太坊 Solidity 中的数据类型

Solidity 数据类型

布尔值

在名称bool和值truefalse下支持布尔类型

数值类型

支持整数类型,有符号和无符号,从 int8/uint8 到 int256/uint256(分别是 8 位整数到 256 位整数)。 类型 uint 是 uint256 的简写(同样,int 是 int256 的简写)。

值得注意的是,支持浮点类型。 为什么不? 好吧,一方面,在处理货币价值时,众所周知,浮点变量是一个坏主意(当然,一般来说),因为价值可能会化为乌有。 以太币值以 wei 表示,即以太币的 1/1,000,000,000,000,000,000 分之一,对于所有用途而言,它必须具有足够的精度; 你不能将以太分解成更小的部分。

目前部分支持定点值。 根据 Solidity 文档: “Solidity 尚未完全支持定点数。 它们可以被声明,但不能被分配给或来自。”

https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9

注意:在大多数情况下,最好只使用 uint,因为减小变量的大小(例如 uint32)实际上会增加 gas 成本,而不是像您预期的那样减少它们。 作为一般经验法则,除非您确定有充分的理由不这样做,否则请使用 uint。

字符串类型

Solidity 中的字符串数据类型是一个有趣的话题; 根据与谁交谈,您可能会得到不同的意见。 Solidity 中有一个字符串数据类型,这是事实。 我的观点,可能大多数人都同意,是它没有提供太多功能。 字符串解析、连接、替换、修剪,甚至计算字符串的长度:您可能期望从字符串类型中获得的那些东西都不存在,因此它们是您的责任(如果您需要它们)。 有些人使用 bytes32 代替字符串; 这也可以做到。

关于 Solidity 字符串的有趣文章

我的意见:编写自己的字符串类型并将其发布以供一般使用可能是一个有趣的练习。

地址类型

或许对于 Solidity 来说是独一无二的,我们有一个地址数据类型,专门用于以太坊钱包或合约地址。 这是一个 20 字节的值,专门用于存储特定大小的地址。 此外,它还具有专门用于此类地址的类型成员。

 address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;

地址数据类型

日期时间类型

Solidity 本身没有原生的 Date 或 DateTime 类型,就像在 JavaScript 中一样。 (哦,不,Solidity 的每段听起来都越来越糟糕!?)日期本身是作为 uint (uint256) 类型的时间戳来处理的。 它们通常作为 Unix 样式的时间戳处理,以秒而不是毫秒为单位,因为块时间戳是 Unix 样式的时间戳。 如果您发现自己出于各种原因需要人类可读的日期,则可以使用开源库。 您可能会注意到我在 BoxingOracle 中使用了一个:DateLib.sol。 OpenZeppelin 还具有日期实用程序以及许多其他类型的通用实用程序库(我们将很快了解 Solidity 的功能)。

专业提示: OpenZeppelin 是知识和预先编写的通用代码的一个很好的来源(但当然不是唯一的好来源),可以帮助您建立合同。

映射

注意 BoxingOracle.sol 的第 11 行定义了一个叫做映射的东西:

 mapping(bytes32 => uint) matchIdToIndex;

Solidity 中的映射是一种用于快速查找的特殊数据类型; 本质上是一个查找表或类似于哈希表,其中包含的数据存在于区块链本身上(当映射被定义为类成员时,就像这里一样)。 在合约执行过程中,我们可以向映射添加数据,类似于向哈希表添加数据,然后查找我们添加的那些值。 再次注意,在这种情况下,我们添加的数据被添加到区块链本身,因此它将保持不变。 如果我们今天将它添加到纽约的地图中,一周后伊斯坦布尔的某个人就可以阅读它。

从 BoxingOracle.sol 的第 71 行添加到映射的示例:

 matchIdToIndex[id] = newIndex+1

从 BoxingOracle.sol 的第 51 行读取映射的示例:

 uint index = matchIdToIndex[_matchId];

也可以从映射中删除项目。 它没有在这个项目中使用,但它看起来像这样:

 delete matchIdToIndex[_matchId];

返回值

您可能已经注意到,Solidity 可能与 Javascript 有一些表面上的相似之处,但它并没有继承 JavaScript 的类型和定义的松散性。 必须以相当严格和受限的方式定义合约代码(考虑到用例,这可能是一件好事)。 考虑到这一点,考虑 BoxingOracle.sol 第 40 行的函数定义

function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }

好的,所以,让我们首先快速概述一下这里包含的内容。 function将其标记为函数。 _getMatchIndex是函数名(下划线是表示私有成员的约定——我们稍后会讨论)。 它接受一个名为_matchId的参数(这次使用下划线约定来表示函数参数),类型为bytes32 。 关键字private实际上使成员在作用域上是私有的, view告诉编译器这个函数不会修改区块链上的任何数据,最后:~~~solidity返回(uint)~~~

这表示该函数返回一个 uint(返回 void 的函数在这里根本没有returns子句)。 为什么 uint 在括号中? 那是因为 Solidity 函数可以并且经常返回tuples

现在考虑第 166 行的以下定义:

 function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner) { ... }

看看这个的退货条款! 它返回一、二……七种不同的东西。 好的,所以,这个函数将这些东西作为一个元组返回。 为什么? 在开发过程中,您经常会发现自己需要返回一个结构体(如果是 JavaScript,您可能希望返回一个 JSON 对象)。 好吧,在撰写本文时(尽管将来可能会改变),Solidity 不支持从公共函数返回结构。 所以你必须返回元组。 如果你是一个 Python 人,你可能已经习惯了元组。 但是,许多语言并不真正支持它们,至少不是以这种方式。

有关将元组作为返回值返回的示例,请参见第 159 行:

 return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);

我们如何接受这样的返回值? 我们可以这样做:

 var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);

或者,您可以使用正确的类型预先显式声明变量:

 //declare the variables bytes32 id; string name; ... etc... int8 winner; //assign their values (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);

现在我们已经声明了 7 个变量来保存 7 个返回值,我们现在可以使用它们了。 否则,假设我们只想要其中的一两个值,我们可以说:

 //declare the variables bytes32 id; uint date; //assign their values (id,,,,date,,) = getMostRecentMatch(false);

看看我们在那里做了什么? 我们只得到了我们感兴趣的两个。查看所有这些逗号。 我们必须仔细计算它们!

进口

BoxingOracle.sol 的第 3 行和第 4 行是导入:

 import "./Ownable.sol"; import "./DateLib.sol";

正如您可能预期的那样,这些是从与 BoxingOracle.sol 相同的合同项目文件夹中存在的代码文件中导入定义。

修饰符

请注意,函数定义附加了一堆修饰符。 首先,有可见性:私有的、公共的、内部的和外部的——功能可见性。

此外,您会看到关键字pureview 。 这些向编译器指示函数将进行何种更改(如果有)。 这很重要,因为这样的事情是运行该功能的最终 gas 成本的一个因素。 有关说明,请参见此处:Solidity Docs。

最后,我真正想讨论的是自定义修饰符。 查看 BoxingOracle.sol 的第 61 行:

 function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {

请注意“public”关键字之前的onlyOwner修饰符。 这表明只有合约的所有者才能调用该方法! 虽然非常重要,但这并不是 Solidity 的原生特性(尽管将来可能会出现)。 实际上, onlyOwner是我们自己创建和使用的自定义修饰符的一个示例。 我们来看一下。

首先,修饰符在文件 Ownable.sol 中定义,您可以看到我们在 BoxingOracle.sol 的第 3 行导入了该文件:

 import "./Ownable.sol"

请注意,为了使用修饰符,我们让BoxingOracle继承自Ownable 。 在 Ownable.sol 的第 25 行,我们可以在“Ownable”合约中找到修饰符的定义:

 modifier onlyOwner() { require(msg.sender == owner); _; }

(顺便说一下,这个 Ownable 合约取自 OpenZeppelin 的公共合约之一。)

注意这个东西被声明为修饰符,表示我们可以照原样使用它来修改函数。 请注意,修饰符的核心是“require”语句。 Require 语句有点像断言,但不适用于调试。 如果 require 语句的条件失败,则该函数将抛出异常。 因此,解释这个“要求”声明:

 require(msg.sender == owner);

我们可以说这意味着:

 if (msg.send != owner) throw an exception;

而且,事实上,在 Solidity 0.4.22 及更高版本中,我们可以在该 require 语句中添加一条错误消息:

 require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only");

最后,在好奇的行中:

 _;

下划线是“这里,执行修改后的函数的全部内容”的简写。 所以实际上, require 语句将首先执行,然后是实际的函数。 因此,这就像将这行逻辑预先添加到修改后的函数中。

当然,你可以用修饰符做更多的事情。 检查文档:文档。

Solidity 库

Solidity 有一个语言特性,称为。 我们在 DateLib.sol 的项目中有一个示例。

Solidity 库实现!

这是一个更容易处理日期类型的库。 它在第 4 行被导入 BoxingOracle:

 import "./DateLib.sol";

它在第 13 行使用:

 using DateLib for DateLib.DateTime;

DateLib.DateTime是从 DateLib 合同中导出的结构(它作为成员公开;参见 DateLib.sol 的第 4 行),我们在这里声明我们正在“使用”特定数据类型的 DateLib 库。 因此,在该库中声明的方法和操作将适用于我们所说的数据类型。 这就是在 Solidity 中使用库的方式。

一个更清楚的例子,查看一些 OpenZeppelin 的数字库,例如 SafeMath。 这些可以应用于本机(数字)Solidity 数据类型(而这里我们将库应用于自定义数据类型),并且被广泛使用。

接口

与主流的面向对象语言一样,支持接口。 Solidity 中的接口被定义为合约,但函数的函数体被省略了。 有关接口定义的示例,请参见 OracleInterface.sol。 在此示例中,该接口用作预言机合约的替代,其内容驻留在具有单独地址的单独合约中。

命名约定

当然,命名约定不是全局规则; 作为程序员,我们知道我们可以自由地遵循吸引我们的编码和命名约定。 另一方面,我们确实希望其他人能够舒适地阅读和使用我们的代码,因此需要一定程度的标准化。

项目概况

所以现在我们已经了解了相关代码文件中存在的一些通用语言特性,我们可以开始更具体地查看这个项目的代码本身。

所以,让我们再次澄清这个项目的目的。 该项目的目的是提供使用预言机的智能合约的半现实(或伪现实)演示和示例。 从本质上讲,这只是一个调用另一个单独合约的合约。

该示例的商业案例可以表述如下:

  • 用户想要在拳击比赛上进行不同规模的投注,为投注支付金钱(以太币),并在他们获胜时以及如果他们获胜时收集他们的奖金。
  • 用户通过智能合约进行这些投注。 (在实际用例中,这将是一个带有 web3 前端的完整 DApp;但我们只检查合同方面。)
  • 一个单独的智能合约——预言机——由第三方维护。 它的工作是维护一个拳击比赛列表及其当前状态(待定、进行中、已完成等),如果已完成,则为获胜者。
  • 主合约从预言机获取待定匹配列表,并将这些作为“可投注”匹配呈现给用户。
  • 主合约在比赛开始前接受投注。
  • 比赛决定后,主合约根据简单的算法分配输赢,自己分红,并根据要求支付奖金(输家只是失去全部赌注)。

投注规则:

  • 有一个已定义的最小赌注(以 wei 定义)。
  • 没有最大赌注; 用户可以在最低限额以上投注任何他们喜欢的金额。
  • 用户可以下注,直到比赛“进行中”。

分配奖金的算法:

  • 所有收到的赌注都放入“底池”。
  • 一小部分从锅中取出,用于房子。
  • 每个获胜者都会获得一定比例的底池,这与他们下注的相对大小成正比。
  • 在比赛决定后,一旦第一个用户请求结果,就会计算奖金。
  • 根据用户的要求奖励奖金。
  • 在平局的情况下,没有人获胜——每个人都拿回了他们的赌注,而且房子没有削减。

BoxingOracle:Oracle 合同

提供的主要功能

预言机有两个接口,你可以说:一个呈现给合约的“所有者”和维护者,一个呈现给公众; 即消耗预言机的合约。 作为维护者,它提供了将数据输入合约的功能,本质上是从外部世界获取数据并将其放到区块链上。 对公众来说,它提供对所述数据的只读访问权限。 值得注意的是,合同本身限制非所有者编辑任何数据,但对该数据的只读访问权限是公开授予的,不受限制。

对用户:

  • 列出所有匹配项
  • 列出待处理的匹配项
  • 获取特定比赛的详细信息
  • 获取特定比赛的状态和结果

致业主:

  • 输入匹配
  • 更改比赛状态
  • 设定比赛结果

用户和所有者访问元素的插图

用户的故事:

  • 5 月 9 日宣布并确认了一场新的拳击比赛。
  • 我,合约的维护者(也许我是一个知名的体育网络或新的网点),将即将到来的比赛添加到区块链上的预言机数据中,状态为“待定”。 任何人或任何合约现在都可以根据自己的喜好查询和使用这些数据。
  • 比赛开始时,我将该比赛的状态设置为“进行中”。
  • 当比赛结束时,我将比赛的状态设置为“完成”并修改比赛数据以表示获胜者。

甲骨文代码审查

这篇评论完全基于 BoxingOracle.sol; 行号引用该文件。

在第 10 行和第 11 行,我们声明了匹配的存储位置:

 Match[] matches; mapping(bytes32 => uint) matchIdToIndex;

matches只是一个用于存储匹配实例的简单数组,映射只是将唯一匹配 ID(bytes32 值)映射到其在数组中的索引的工具,因此如果有人给我们匹配的原始 ID,我们可以使用此映射来定位它。

在第 17 行,我们定义并解释了匹配结构:

 //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 }

第 61 行:函数addMatch仅供合约所有者使用; 它允许为存储的数据添加新的匹配项。

第 80 行:函数declareOutcome允许合约所有者将匹配设置为“已确定”,设置获胜的参与者。

第 102-166 行:以下函数都可以被公众调用。 这是一般向公众开放的只读数据:

  • 函数getPendingMatches返回当前状态为“待定”的所有匹配项的 ID 列表。
  • 函数getAllMatches返回所有匹配项的 ID 列表。
  • 函数getMatch返回由 ID 指定的单个匹配项的完整详细信息。

第 193-204 行声明了主要用于测试、调试和诊断的函数。

  • 函数testConnection只是测试我们是否能够调用合约。
  • 函数getAddress返回此合约的地址。
  • 函数addTestData将一堆测试匹配添加到匹配列表中。

在继续下一步之前,请随意探索一下代码。 我建议在调试模式下再次运行 oracle 合约(如本系列的第 1 部分所述),调用不同的函数并检查结果。

BoxingBets:客户合同

定义客户合同(投注合同)负责什么以及不负责什么是很重要的。 客户合同负责维护真实拳击比赛的列表或宣布其结果。 我们“信任”(是的,我知道,有一个敏感词——呃,哦——我们将在第 3 部分中讨论)该服务的预言机。 客户合约负责接受投注。 它负责分配奖金并根据比赛结果(从预言机收到)将奖金转移到获胜者账户的算法。

此外,一切都是基于拉的,没有事件或推动。 合约从预言机中提取数据。 合约从预言机中提取匹配结果(响应用户请求),合约计算奖金并根据用户请求进行转移。

提供的主要功能

  • 列出所有待处理的匹配项
  • 获取特定比赛的详细信息
  • 获取特定比赛的状态和结果
  • 下注
  • 请求/接收奖金

客户代码审查

此评论完全基于 BoxingBets.sol; 行号引用该文件。

第 12 行和第 13 行是合约中的第一行代码,定义了一些映射,我们将在其中存储合约数据。

第 12 行将用户地址映射到 ID 列表。 这是将用户映射到属于该用户的投注 ID 列表。 因此,对于任何给定的用户地址,我们可以快速获取该用户已进行的所有投注的列表。

 mapping(address => bytes32[]) private userToBets;

第 13 行将比赛的唯一 ID 映射到下注实例列表。 有了这个,对于任何给定的比赛,我们都可以获取为该比赛所做的所有投注的列表。

 mapping(bytes32 => Bet[]) private matchToBets;

第 17 和 18 行与我们的 oracle 的连接有关。 首先,在boxingOracleAddr变量中,我们存储了预言机合约的地址(默认设置为零)。 我们可以对预言机的地址进行硬编码,但我们永远无法更改它。 (不能更改预言机的地址可能是好事也可能是坏事——我们可以在第 3 部分讨论)。 下一行创建一个 oracle 接口的实例(在 OracleInterface.sol 中定义)并将其存储在一个变量中。

 //boxing results oracle address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);

如果您跳到第 58 行,您将看到setOracleAddress函数,在该函数中可以更改此预言机地址,并且使用新地址重新实例化boxingOracle实例。

第 21 行定义了我们的最小下注规模,单位为 wei。 这当然是一个非常小的数量,只有 0.000001 以太币。

 uint internal minimumBet = 1000000000000;

分别在第 58 和 66 行,我们有setOracleAddressgetOracleAddress函数。 setOracleAddress具有onlyOwner修饰符,因为只有合约的所有者才能将预言机切换到另一个预言机(可能不是一个好主意,但我们将在第 3 部分中详细说明)。 另一方面, getOracleAddress函数是可公开调用的; 任何人都可以看到正在使用什么预言机。

 function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {... function getOracleAddress() external view returns (address) { ....

在第 72 和 79 行,我们分别有getBettableMatchesgetMatch函数。 请注意,这些只是将调用转发到 oracle 并返回结果。

 function getBettableMatches() public view returns (bytes32[]) {... function getMatch(bytes32 _matchId) public view returns ( ....

placeBet函数是一个非常重要的函数(第 108 行)。

 function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...

这个的一个显着特征是payable修饰符; 我们一直忙于讨论通用语言特性,以至于我们还没有触及能够与函数调用一起汇款的核心重要特性! 基本上就是这样——它是一个可以接受一定数量的钱以及发送的任何其他参数和数据的函数。

我们在这里需要这个,因为这是用户同时定义他们将要进行什么赌注、他们打算在该赌注上获得多少钱以及实际汇款的地方。 payable修饰符可以实现这一点。 在接受投注之前,我们会进行一系列检查以确保投注的有效性。 第 111 行的第一个检查是:

 require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");

发送的金额存储在msg.value中。 假设所有检查都通过,在第 123 行,我们将把该金额转移到预言机的所有权中,将该金额的所有权从用户手中夺走,并归合约所有:

 address(this).transfer(msg.value);

最后,在第 136 行,我们有一个测试/调试辅助函数,它将帮助我们了解合约是否连接到有效的预言机:

 function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }

包起来

这实际上就是这个例子。 只是接受赌注。 分配奖金和支付的功能以及其他一些逻辑被故意省略,以使示例对我们的目的足够简单,这只是为了演示如何使用带有合约的预言机。 更完整、更复杂的逻辑目前存在于另一个项目中,它是这个例子的扩展,还在开发中。

因此,现在我们对代码库有了更好的理解,并将其用作讨论 Solidity 提供的一些语言特性的载体和起点。 这个由三部分组成的系列的主要目的是演示和讨论合约与预言机的使用。 这部分的目的是更好地理解这个特定的代码,并将其作为理解 Solidity 和智能合约开发的一些特性的起点。 第三部分也是最后一部分的目的是讨论预言机使用的策略和理念,以及它如何在概念上适合智能合约模型。

进一步的可选步骤

我强烈鼓励希望了解更多信息的读者使用此代码并使用它。 实施新功能。 修复任何错误。 实现未实现的功能(例如支付接口)。 测试函数调用。 修改它们并重新测试以查看会发生什么。 添加一个 web3 前端。 添加用于删除匹配项或修改其结果的工具(以防出错)。 取消的比赛怎么办? 实施第二个预言机。 当然,合约可以随意使用任意数量的预言机,但这会带来什么问题呢? 玩得开心; 这是一种很好的学习方式,当你这样做(并从中获得乐趣)时,你一定会记住更多你学到的东西。

要尝试的示例,非全面列表:

  • 在本地测试网中运行合约和预言机(在 truffle 中,如第 1 部分所述)并调用所有可调用函数和所有测试函数。
  • 添加用于计算奖金并在比赛结束时支付奖金的功能。
  • 添加在平局时退还所有投注的功能。
  • 添加一项功能以在比赛开始前请求退款或取消投注。
  • 添加一项功能以允许比赛有时会被取消(在这种情况下,每个人都需要退款)。
  • 实施一项功能,以确保用户下注时的预言机与将用于确定比赛结果的预言机相同。
  • 实现另一个(第二个)oracle,它具有一些与之相关的不同功能,或者可能服务于拳击以外的运动(请注意,参与者计数和列表允许不同类型的运动,因此我们实际上并不仅限于拳击) .
  • 实现getMostRecentMatch以便它实际上返回最近添加的匹配,或者就发生时间而言最接近当前日期的匹配。
  • 实现异常处理。

一旦您熟悉了合约和预言机之间的关系机制,在这个由三部分组成的系列的第 3 部分中,我们将讨论这个示例提出的一些战略、设计和哲学问题。