本文是Solidity智能合约实例系列的继续,它实现了一个有点复杂的盲拍过程。
在这里,我们要走过一个盲目拍卖的例子(原文)。
- 我们将首先列出整个智能合约的例子,为了可读性和开发目的,不加注释。
- 然后,我们将逐一剖析它,分析它,解释它。
- 按照这个路径,我们将获得智能合约的实践经验,以及编码、理解和调试智能合约的良好实践。
智能合约--盲目拍卖
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract BlindAuction {
struct Bid {
bytes32 blindedBid;
uint deposit;
}
address payable public beneficiary;
uint public biddingEnd;
uint public revealEnd;
bool public ended;
mapping(address => Bid[]) public bids;
address public highestBidder;
uint public highestBid;
mapping(address => uint) pendingReturns;
event AuctionEnded(address winner, uint highestBid);
error TooEarly(uint time);
error TooLate(uint time);
error AuctionEndAlreadyCalled();
modifier onlyBefore(uint time) {
if (block.timestamp >= time) revert TooLate(time - block.timestamp);
_;
}
modifier onlyAfter(uint time) {
if (block.timestamp <= time) revert TooEarly(time - block.timestamp);
_;
}
constructor(
uint biddingTime,
uint revealTime,
address payable beneficiaryAddress
) {
beneficiary = beneficiaryAddress;
biddingEnd = block.timestamp + biddingTime;
revealEnd = biddingEnd + revealTime;
}
function blind_a_bid(uint value, bool fake, bytes32 secret)
public
pure
returns (bytes32){
return keccak256(abi.encodePacked(value, fake, secret));
}
function bid(bytes32 blindedBid)
external
payable
onlyBefore(biddingEnd)
{
bids[msg.sender].push(Bid({
blindedBid: blindedBid,
deposit: msg.value
}));
}
function reveal(
uint[] calldata values,
bool[] calldata fakes,
bytes32[] calldata secrets
)
external
onlyAfter(biddingEnd)
onlyBefore(revealEnd)
{
uint length = bids[msg.sender].length;
require(values.length == length);
require(fakes.length == length);
require(secrets.length == length);
uint refund;
for (uint i = 0; i < length; i++) {
Bid storage bidToCheck = bids[msg.sender][i];
(uint value, bool fake, bytes32 secret) =
(values[i], fakes[i], secrets[i]);
if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
continue;
}
refund += bidToCheck.deposit;
if (!fake && bidToCheck.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
bidToCheck.blindedBid = bytes32(0);
}
payable(msg.sender).transfer(refund);
}
function withdraw() external {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
pendingReturns[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
function auctionEnd()
external
onlyAfter(revealEnd)
{
if (ended) revert AuctionEndAlreadyCalled();
emit AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
function placeBid(address bidder, uint value) internal
returns (bool success)
{
if (value <= highestBid) {
return false;
}
if (highestBidder != address(0)) {
pendingReturns[highestBidder] += highestBid;
}
highestBid = value;
highestBidder = bidder;
return true;
}
}
代码分解和分析
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract BlindAuction {
用于存储盲目竞标者及其保证金数据的数据结构。
struct Bid {
bytes32 blindedBid;
uint deposit;
}
存储受益人地址和整数偏移的状态变量,用于计算竞价结束时间、最佳出价揭示结束时间,以及标志拍卖结束的标志。
address payable public beneficiary;
uint public biddingEnd;
uint public revealEnd;
bool public ended;
一个映射数据结构,存储每个竞标者的所有个人盲标。
mapping(address => Bid[]) public bids;
最高出价和最高出价者的数据。
address public highestBidder;
uint public highestBid;
出价过高的竞标将被允许撤回。
mapping(address => uint) pendingReturns;
事件声明标志着拍卖结束,错误涵盖了过早、过晚或在拍卖结束后调用一个函数的情况。
/// The function has been called too early.
/// Try again at `time`.
error TooEarly(uint time);
/// The function has been called too late.
/// It cannot be called after `time`.
error TooLate(uint time);
/// The function auctionEnd has already been called.
error AuctionEndAlreadyCalled();
我们使用修改器来验证函数的参数,即函数执行时刻的时间戳。这样一来,我们就能确保函数按顺序执行,并在拍卖的适当阶段,即合同的生命周期内执行。
修改器的工作原理是将合并通配符_; ,然后执行其原始代码和合并后的代码,以取代函数的主体。
合并通配符可以放在修改器代码的前面、中间或后面。
modifier onlyBefore(uint time) {
if (block.timestamp >= time) revert TooLate(time - block.timestamp);
_;
}
modifier onlyAfter(uint time) {
if (block.timestamp <= time) revert TooEarly(time - block.timestamp);
_;
}
我们熟悉的朋友,构造函数是一个特殊的函数,对每个合同只执行一次,在其创建/初始化期间。
构造函数设置受益人地址,并计算出竞价阶段和揭示阶段的结束时间。
constructor(
uint biddingTime,
uint revealTime,
address payable beneficiaryAddress
) {
beneficiary = beneficiaryAddress;
biddingEnd = block.timestamp + biddingTime;
revealEnd = biddingEnd + revealTime;
}
blind_a_bid 函数是我对 Solidity 文档中的原始例子的补充。否则的话,把它放在盲拍智能合约中是没有意义的,但我把它放在这里是为了帮助我们计算盲标。
function blind_a_bid(uint value, bool fake, bytes32 secret)
public
pure
returns (bytes32){
return keccak256(abi.encodePacked(value, fake, secret));
}
一个出价有两种方式是盲目的。
- 出价由keccak256哈希函数产生的哈希值表示,这是SHA-3哈希算法的一个实现。在其他信息中,投标哈希值隐藏了投标值。在以后的时间里,当出价被披露时,才有可能知道出价值。到目前为止,只有押金是已知的,并要求投标人通过与投标书一起交纳一定数额的押金来做出承诺。
- 投标人的押金可能与所附的投标价值不同,这可能会使竞争者根据押金来估计投标价值而感到困惑。投标价值不能超过押金,但可以低至0。另一个散列的信息是一个假标志,用来故意使投标无效。另一种情况是当出价不超过最高出价时被认为是无效的。
function bid(bytes32 blindedBid)
external
payable
这里我们使用一个修改器onlyBefore ,参数为biddingEnd 。
onlyBefore(biddingEnd)
{
该投标被放入我们的映射数据结构中,供以后处理。目前,投标仍然是封闭的,只有存款是已知的。
bids[msg.sender].push(Bid({
blindedBid: blindedBid,
deposit: msg.value
}));
}
reveal(...) 函数是这个智能合约的核心功能:它揭示了被蒙蔽的投标,并将正确蒙蔽的无效投标和除最高投标外的所有投标退还。
/// Reveal your blinded bids. You will get a refund for all
/// correctly blinded invalid bids and for all bids except for
/// the totally highest.
function reveal(
我们的参数是复杂的参数,因此我们需要明确说明其内存区域,即calldata 。
uint[] calldata values,
bool[] calldata fakes,
bytes32[] calldata secrets
)
该函数必须在竞价结束后和揭晓结束前可以调用。
external
onlyAfter(biddingEnd)
onlyBefore(revealEnd)
{
函数调用者/竞标者的每个出价都被处理,出价列表的长度(为每个竞标者计算)被存储在变量长度中。
为了处理和揭示出价,我们需要有相同数量的明确的、未被掩盖的参数,即变量值、假象和秘密。
uint length = bids[msg.sender].length;
require(values.length == length);
require(fakes.length == length);
require(secrets.length == length);
uint refund;
迭代通过函数调用者/投标者的每个投标。
for (uint i = 0; i < length; i++) {
为了更方便的访问,定义了对标的的引用。
Bid storage bidToCheck = bids[msg.sender][i];
定义了对输入参数元素的引用,以便更方便地访问。
(uint value, bool fake, bytes32 secret) =
(values[i], fakes[i], secrets[i]);
根据三个参数:一个(真正的投标)值,一个假的标志和一个秘密,将收到的盲标与一个新的盲标进行比较。如果结果与收到的盲标不同,则不透露投标,跳过退款。
信息:在纯数学意义上,不可能揭示散列背后的内容,因为散列函数的结果是不可逆的,也就是说,不可能从散列结果中推断出原始数据。
然而,我们可以对我们假设代表原始数据的参数进行散列,如果我们得到相同的散列结果,我们就知道原始数据是正确的,与盲标背后的数据相匹配。
从理论上讲,同一个哈希函数的多个输入可能会产生相同的哈希结果;哈希函数的这一特性被称为*哈希碰撞。然而,对于keccak256,以及SHA-3这个更广泛的系列,由于其固有的抗哈希碰撞*的特性,哈希碰撞的可能性只是理论上的。
如果哈希值、假币和密文与盲标不一致,则不透露标的,跳过退款。
if (bidToCheck.blindedBid != keccak256(abi.encodePacked(value, fake, secret))) {
continue;
}
如果出价被揭露,押金会被加到总金额中,以获得退款。
refund += bidToCheck.deposit;
有效的出价值会从总金额中扣除,剩余的部分可供提取。
if (!fake && bidToCheck.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
撤回的投标通过将盲标设置为0而被阻止重复提取。理论上,投标人不可能向reveal() 函数发送正确的价值、假货和秘密参数,以匹配设置为bytes32(0) 的盲标。
bidToCheck.blindedBid = bytes32(0);
}
将退款,即扣除成功出价后的剩余部分,返还给出价人。
payable(msg.sender).transfer(refund);
}
撤回一个特定投标人的所有先前有效(最高)的投标。
/// Withdraw a bid that was overbid.
function withdraw() external {
将某一投标人的待定回报(msg.sender)读入amount 变量。
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
我们应该把pendingReturns 变量设为 0,否则,投标人/收款人有可能在transfer() 函数返回之前重新调用withdraw() 函数,并通过这样做,再次要求提款。
通过将pendingReturns 变量设置为0,我们实现了一个条件→行动(效果)→互动的设计模式,防止了这种不需要的行为。
pendingReturns[msg.sender] = 0;
将悬而未决的回报转移给投标者/接受者。
payable(msg.sender).transfer(amount);
}
}
注意:Solidity官方文档建议将交互功能分为三个功能部分。
- 检查条件。
- 执行动作(效果),以及
- 与其他合约的交互。
auctionEnd() 函数结束拍卖并将最高出价发送给受益人。
function auctionEnd()
external
onlyAfter(revealEnd)
{
检查条件...
if (ended) revert AuctionEndAlreadyCalled();
...执行行动(效果)...
emit AuctionEnded(highestBidder, highestBid);
ended = true;
...并与其他合同进行互动。
beneficiary.transfer(highestBid);
}
当出价有效时,placeBid(...) 函数从reveal(...) 函数内部调用。内部函数的可见性使得该函数只能从原始或派生合约的内部使用。
function placeBid(address bidder, uint value) internal
returns (bool success)
{
如果一个出价超过最高出价,那么它将成为新的最高出价。否则,该投标将被跳过。
if (value <= highestBid) {
return false;
}
之前的最高出价者被跳过,他的出价会被加到他之前的出价中,以备退款。直接退款被认为是一种安全风险,因为有可能执行一个不受信任的合同。
相反,投标人(收件人)将通过使用下面的withdraw() 函数自己撤回他们的投标。
if (highestBidder != address(0)) {
pendingReturns[highestBidder] += highestBid;
}
新的最高出价人和他的出价被记录下来;事件HighestBidIncreased ,携带这个信息对被发射出来。
highestBid = value;
highestBidder = bidder;
return true;
}
}
我们的智能合约的盲目拍卖的例子比上一个例子,即简单的公开拍卖要复杂一些。
盲目拍卖的例子使我们能够向受益人出价一定数量的货币,同时防止其他竞标者看到确切的数量。我们通过掩盖出价来达到这个目的,只留下公开可见的存款。
然而,出价的可见性将在以后的揭示阶段被允许。出价金额可以低于或等于定金,出价也可以通过设置假标志为真而故意无效。
当合同通过其构造函数实例化时,它设置了拍卖结束时间,揭示结束时间,以及其受益人,即受益人地址。该合同有五个功能,通过专门的函数实现:出价、揭示、下标、撤标和结束拍卖。
注意:竞标和下标的区别在于,竞标只将一个标的放入竞标者的组合中,即映射标的,而下标只取有效标的,并将其与最高标的进行验证,就像在简单的公开竞标例子中那样。
只有当新出价的金额严格大于当前最高出价时才会被接受。一个新的出价被接受意味着当前的最高出价被添加到投标人的余额中,以便以后提取。
新的最高出价人成为当前最高出价人,新的最高出价成为当前最高出价。
提取投标会将之前所有加起来的投标返回给每个投标人(映射 pendingReturns)。
附录--合同参数
本节包含了运行合同的额外信息。我们应该想到,我们的例子账户可能会随着Remix的每次刷新/重新加载而改变。
我们的合同创建参数是投标阶段 (秒)、揭示阶段(秒)和受益人地址(部署实例时复制这一行)。
300, 300, 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
合同测试场景
我利用了这个优秀的网络应用来生成秘密(64位)。 https://www.browserling.com/tools/random-hex
- 公开拍卖时间(以秒为单位)。
300 - 揭晓时间长度(秒)。
300 - 受益人。
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
测试/演示步骤。
竞标阶段
- 账户0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2盲目出价10魏,并存入12魏。
0xfee8f88b6d146b9c01c8451a51c151d719db0e8965abcd4f27a9c91833e16a6b
- 帐户0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db盲目出价25魏,存款30魏。
0x108872ad459cb73a884d99cd2ddf8f583cbe77a500888f530b2ea6a806258749
- 帐户0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB假意出价30魏,存入30魏。
0x02e3f010253a21db97ad6c302f7de9fdf65d0eb6931cfb4532f754afa1612528
- 帐户0x617F2E2fD72FD9D5503197092aC168c91465E7f2盲目出价30魏,并存入20魏。
0xd6d6a208ad6a4eca9538b70f6d28d6472418c776852d8cab6109969a12e4c6b2
揭幕阶段
- 帐户0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2透露他的出价。
[10], [false], [0xc57dae203273d61da1d7d78275618ff13e78022a9ace0a9b43ec3e95377f3ed2]
- 帐户0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db透露他的出价。
[25], [false], [0xeb944b2bab8f46fa9f77d1cd2cb84285b026c4d4038386865457a9df563c54cc]
- 帐户0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB显示他的出价。
[30], [true], [0xa3b1f6694045af12ac5f3cfbf775032e9df24fc337e93ae378886275b8f4cd03]
- 帐户0x617F2E2fD72FD9D5503197092aC168c91465E7f2显示他的出价。
[30], [false], [0xc5cf9cfe9230f16beb84ea417453c330366decc7378b9cd78db68ce1fb4a242b]
结论
我们继续我们的智能合约例子系列,这篇文章实现了一个盲目的拍卖。
首先,为了可读性,我们布置了干净的源代码(没有任何注释)。谁需要注释,看一下矩阵就够了...
其次,我们剖析了代码,对其进行了分析,并解释了每个可能的非琐碎部分。或者说我们有吗?
