Solidity-07-Solidity by Example
投票
以下合约相当复杂,但展示了 Solidity 的许多功能。
它实现了一个投票合约。
当然,电子投票的主要问题是如何将投票权分配给正确的人以及如何防止操纵。
我们不会在这里解决所有问题,但至少我们将展示如何进行委托投票,以便同时自动计算投票并且完全透明。
这个想法是为每张选票创建一份合同,为每个选项提供一个短名称。
然后,担任主席的合约的创建者将分别授予每个地址的投票权。
然后,地址背后的人可以选择自己投票或将投票委托给他们信任的人。
在投票时间结束时,winningProposal() 将返回得票最多的提案。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 Voter) public voters;
// A dynamically-sized array of `Proposal` structs.
Proposal[] public proposals;
/// Create a new ballot to choose one of `proposalNames`.
constructor(bytes32[] memory proposalNames) {
chairperson = msg.sender;
voters[chairperson].weight = 1;
// For each of the provided proposal names,
// create a new proposal object and add it
// to the end of the array.
for (uint i = 0; i = 1);
sender.voted = true;
sender.delegate = to;
if (delegate_.voted) {
// If the delegate already voted,
// directly add to the number of votes
proposals[delegate_.vote].voteCount += sender.weight;
} else {
// If the delegate did not vote yet,
// add to her weight.
delegate_.weight += sender.weight;
}
}
/// Give your vote (including votes delegated to you)
/// to proposal `proposals[proposal].name`.
function vote(uint proposal) external {
Voter storage sender = voters[msg.sender];
require(sender.weight != 0, "Has no right to vote");
require(!sender.voted, "Already voted.");
sender.voted = true;
sender.vote = proposal;
// If `proposal` is out of the range of the array,
// this will throw automatically and revert all
// changes.
proposals[proposal].voteCount += sender.weight;
}
/// @dev Computes the winning proposal taking all
/// previous votes into account.
function winningProposal() public view
returns (uint winningProposal_)
{
uint winningVoteCount = 0;
for (uint p = 0; p winningVoteCount) {
winningVoteCount = proposals[p].voteCount;
winningProposal_ = p;
}
}
}
// Calls winningProposal() function to get the index
// of the winner contained in the proposals array and then
// returns the name of the winner
function winnerName() external view
returns (bytes32 winnerName_)
{
winnerName_ = proposals[winningProposal()].name;
}
}
可能的改进
目前,需要许多交易才能将投票权分配给所有参与者。
你能想出更好的方法吗?
盲拍 Blind Auction
在本节中,我们将展示在以太坊上创建一个完全盲目的拍卖合约是多么容易。
我们将从公开拍卖开始,每个人都可以看到出价,然后将此合同扩展到盲拍,直到投标期结束才能看到实际出价。
简单公开拍卖
以下简单拍卖合约的总体思路是,每个人都可以在一个投标期间发送他们的投标。
出价已经包括汇款/以太币,以便将投标人绑定到他们的出价。
如果提出最高出价,则先前的最高出价者将收回他们的钱。
投标期结束后,必须手动调用合同让受益人收到他们的钱——合同无法自行激活。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract SimpleAuction {
// Parameters of the auction. Times are either
// absolute unix timestamps (seconds since 1970-01-01)
// or time periods in seconds.
address payable public beneficiary;
uint public auctionEndTime;
// Current state of the auction.
address public highestBidder;
uint public highestBid;
// Allowed withdrawals of previous bids
mapping(address => uint) pendingReturns;
// Set to true at the end, disallows any change.
// By default initialized to `false`.
bool ended;
// Events that will be emitted on changes.
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);
// Errors that describe failures.
// The triple-slash comments are so-called natspec
// comments. They will be shown when the user
// is asked to confirm a transaction or
// when an error is displayed.
/// The auction has already ended.
error AuctionAlreadyEnded();
/// There is already a higher or equal bid.
error BidNotHighEnough(uint highestBid);
/// The auction has not ended yet.
error AuctionNotYetEnded();
/// The function auctionEnd has already been called.
error AuctionEndAlreadyCalled();
/// Create a simple auction with `biddingTime`
/// seconds bidding time on behalf of the
/// beneficiary address `beneficiaryAddress`.
constructor(
uint biddingTime,
address payable beneficiaryAddress
) {
beneficiary = beneficiaryAddress;
auctionEndTime = block.timestamp + biddingTime;
}
/// Bid on the auction with the value sent
/// together with this transaction.
/// The value will only be refunded if the
/// auction is not won.
function bid() external payable {
// No arguments are necessary, all
// information is already part of
// the transaction. The keyword payable
// is required for the function to
// be able to receive Ether.
// Revert the call if the bidding
// period is over.
if (block.timestamp > auctionEndTime)
revert AuctionAlreadyEnded();
// If the bid is not higher, send the
// money back (the revert statement
// will revert all changes in this
// function execution including
// it having received the money).
if (msg.value 0) {
// It is important to set this to zero because the recipient
// can call this function again as part of the receiving call
// before `send` returns.
pendingReturns[msg.sender] = 0;
// msg.sender is not of type `address payable` and must be
// explicitly converted using `payable(msg.sender)` in order
// use the member function `send()`.
if (!payable(msg.sender).send(amount)) {
// No need to call throw here, just reset the amount owing
pendingReturns[msg.sender] = amount;
return false;
}
}
return true;
}
/// End the auction and send the highest bid
/// to the beneficiary.
function auctionEnd() external {
// It is a good guideline to structure functions that interact
// with other contracts (i.e. they call functions or send Ether)
// into three phases:
// 1. checking conditions
// 2. performing actions (potentially changing conditions)
// 3. interacting with other contracts
// If these phases are mixed up, the other contract could call
// back into the current contract and modify the state or cause
// effects (ether payout) to be performed multiple times.
// If functions called internally include interaction with external
// contracts, they also have to be considered interaction with
// external contracts.
// 1. Conditions
if (block.timestamp Bid[]) public bids;
address public highestBidder;
uint public highestBid;
// Allowed withdrawals of previous bids
mapping(address => uint) pendingReturns;
event AuctionEnded(address winner, uint highestBid);
// Errors that describe failures.
/// 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();
// Modifiers are a convenient way to validate inputs to
// functions. `onlyBefore` is applied to `bid` below:
// The new function body is the modifier's body where
// `_` is replaced by the old function body.
modifier onlyBefore(uint time) {
if (block.timestamp >= time) revert TooLate(time);
_;
}
modifier onlyAfter(uint time) {
if (block.timestamp = value) {
if (placeBid(msg.sender, value))
refund -= value;
}
// Make it impossible for the sender to re-claim
// the same deposit.
bidToCheck.blindedBid = bytes32(0);
}
payable(msg.sender).transfer(refund);
}
/// Withdraw a bid that was overbid.
function withdraw() external {
uint amount = pendingReturns[msg.sender];
if (amount > 0) {
// It is important to set this to zero because the recipient
// can call this function again as part of the receiving call
// before `transfer` returns (see the remark above about
// conditions -> effects -> interaction).
pendingReturns[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
}
/// End the auction and send the highest bid
/// to the beneficiary.
function auctionEnd()
external
onlyAfter(revealEnd)
{
if (ended) revert AuctionEndAlreadyCalled();
emit AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
// This is an "internal" function which means that it
// can only be called from the contract itself (or from
// derived contracts).
function placeBid(address bidder, uint value) internal
returns (bool success)
{
if (value =0.7.0 bool) usedNonces;
constructor() payable {}
function claimPayment(uint256 amount, uint256 nonce, bytes memory signature) external {
require(!usedNonces[nonce]);
usedNonces[nonce] = true;
// this recreates the message that was signed on the client
bytes32 message = prefixed(keccak256(abi.encodePacked(msg.sender, amount, nonce, this)));
require(recoverSigner(message, signature) == owner);
payable(msg.sender).transfer(amount);
}
/// destroy the contract and reclaim the leftover funds.
function shutdown() external {
require(msg.sender == owner);
selfdestruct(payable(msg.sender));
}
/// signature methods.
function splitSignature(bytes memory sig)
internal
pure
returns (uint8 v, bytes32 r, bytes32 s)
{
require(sig.length == 65);
assembly {
// first 32 bytes, after the length prefix.
r := mload(add(sig, 32))
// second 32 bytes.
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes).
v := byte(0, mload(add(sig, 96)))
}
return (v, r, s);
}
function recoverSigner(bytes32 message, bytes memory sig)
internal
pure
returns (address)
{
(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);
return ecrecover(message, v, r, s);
}
/// builds a prefixed hash to mimic the behavior of eth_sign.
function prefixed(bytes32 hash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
}
编写一个简单的支付渠道
Alice 现在构建了一个简单但完整的支付通道实现。
支付渠道使用加密签名来安全、即时且无交易费用地重复传输以太币。
什么是支付渠道?
支付渠道允许参与者在不使用交易的情况下重复转移以太币。
这意味着您可以避免与交易相关的延迟和费用。
我们将探索两方(Alice 和 Bob)之间的简单单向支付渠道。
它包括三个步骤:
Alice 用 Ether 为智能合约提供资金。 这“打开”了支付渠道。
爱丽丝签署消息,指定欠接收者多少以太币。 每次付款都重复此步骤。
Bob“关闭”支付通道,提取他的部分以太币并将剩余部分发送回发送者。
- NOTE
只有第 1 步和第 3 步需要以太坊交易,第 2 步意味着发件人通过链下方法(例如电子邮件)向收件人发送加密签名的消息。
这意味着只需要两笔交易即可支持任意数量的转账。
Bob 可以保证收到他的资金,因为智能合约托管了以太币并兑现了有效的签名消息。
智能合约还强制执行超时,因此即使接收者拒绝关闭通道,爱丽丝也可以保证最终收回她的资金。
由支付渠道的参与者决定保持开放多长时间。
对于短期交易,例如为每分钟网络访问支付网吧费用,支付渠道可能会在有限的时间内保持开放。
另一方面,对于经常性支付,例如支付员工小时工资,支付渠道可能会保持开放数月或数年。
开通支付渠道
为了打开支付通道,Alice 部署了智能合约,附加要托管的 Ether,并指定预期的接收者和通道存在的最长持续时间。
这是合约中的 SimplePaymentChannel 函数,位于本节末尾。
付款
爱丽丝通过向鲍勃发送签名消息来付款。此步骤完全在以太坊网络之外执行。消息由发件人加密签名,然后直接传输给收件人。
每条消息都包含以下信息:
智能合约的地址,用于防止跨合约重放攻击。
到目前为止欠收款人的以太币总量。
在一系列转账结束时,支付通道仅关闭一次。因此,只有一条发送的消息被兑换。这就是为什么每条消息都指定了累积的 Ether 欠款总额,而不是单个小额支付的金额。收件人自然会选择兑换最近的消息,因为那是总数最高的消息。
不再需要每条消息的随机数,因为智能合约只接受一条消息。智能合约的地址仍用于防止用于一个支付渠道的消息被用于不同的渠道。
这是修改后的 JavaScript 代码,用于对上一节中的消息进行加密签名:
function constructPaymentMessage(contractAddress, amount) {
return abi.soliditySHA3(
["address", "uint256"],
[contractAddress, amount]
);
}
function signMessage(message, callback) {
web3.eth.personal.sign(
"0x" + message.toString("hex"),
web3.eth.defaultAccount,
callback
);
}
// contractAddress is used to prevent cross-contract replay attacks.
// amount, in wei, specifies how much Ether should be sent.
function signPayment(contractAddress, amount, callback) {
var message = constructPaymentMessage(contractAddress, amount);
signMessage(message, callback);
}
关闭支付渠道
当 Bob 准备好接收他的资金时,是时候通过调用智能合约上的关闭函数来关闭支付通道了。
关闭通道会向接收者支付他们所欠的以太币并销毁合约,将剩余的以太币发送回爱丽丝。
要关闭通道,Bob 需要提供由 Alice 签名的消息。
智能合约必须验证消息是否包含来自发件人的有效签名。进行此验证的过程与收件人使用的过程相同。
Solidity 函数 isValidSignature 和 recoverSigner 的工作方式与上一节中的 JavaScript 对应函数一样,后者函数是从 ReceiverPays 合约中借用的。
只有支付通道接收方可以调用 close 函数,他们自然会传递最近的支付消息,因为该消息带有最高的总欠款。
如果发件人被允许调用这个函数,他们可以提供一个较低金额的消息,并欺骗收件人他们欠他们的东西。
该函数验证签名消息与给定参数匹配。如果一切顺利,接收者将收到他们的部分以太币,而发送者则通过自毁发送其余部分。
你可以在完整的合约中看到 close 函数。
支付渠道到期
Bob 可以随时关闭支付通道,但如果他们不这样做,Alice 需要一种方法来收回她的托管资金。
在合约部署时设置了到期时间。
一旦达到该时间,Alice 就可以调用 claimTimeout 来收回她的资金。
您可以在完整的合约中看到 claimTimeout 函数。
调用此函数后,Bob 将无法再接收任何 Ether,因此 Bob 在到期之前关闭通道非常重要。
The full contract
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 expiration);
expiration = newExpiration;
}
/// if the timeout is reached without the recipient closing the channel,
/// then the Ether is released back to the sender.
function claimTimeout() external {
require(block.timestamp >= expiration);
selfdestruct(sender);
}
function isValidSignature(uint256 amount, bytes memory signature)
internal
view
returns (bool)
{
bytes32 message = prefixed(keccak256(abi.encodePacked(this, amount)));
// check that the signature is from the payment sender
return recoverSigner(message, signature) == sender;
}
/// All functions below this are just taken from the chapter
/// 'creating and verifying signatures' chapter.
function splitSignature(bytes memory sig)
internal
pure
returns (uint8 v, bytes32 r, bytes32 s)
{
require(sig.length == 65);
assembly {
// first 32 bytes, after the length prefix
r := mload(add(sig, 32))
// second 32 bytes
s := mload(add(sig, 64))
// final byte (first byte of the next 32 bytes)
v := byte(0, mload(add(sig, 96)))
}
return (v, r, s);
}
function recoverSigner(bytes32 message, bytes memory sig)
internal
pure
returns (address)
{
(uint8 v, bytes32 r, bytes32 s) = splitSignature(sig);
return ecrecover(message, v, r, s);
}
/// builds a prefixed hash to mimic the behavior of eth_sign.
function prefixed(bytes32 hash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}
}
- NOTE
函数 splitSignature 不使用所有安全检查。 真正的实现应该使用经过更严格测试的库,例如该代码的 openzepplin 版本。
验证付款
与上一节不同,支付渠道中的消息不会立即兑现。 收件人会跟踪最新消息,并在需要关闭支付渠道时兑现。 这意味着收件人对每条消息执行自己的验证至关重要。 否则无法保证收款人最终能够获得付款。
收件人应使用以下过程验证每条消息:
验证消息中的合约地址是否与支付渠道匹配。
验证新总数是否为预期金额。
验证新的总量不超过托管的以太币数量。
验证签名是否有效并且来自支付渠道发件人。
我们将使用 ethereumjs-util 库来编写此验证。 最后一步可以通过多种方式完成,我们使用 JavaScript。 以下代码从上面的签名 JavaScript 代码中借用了constructPaymentMessage 函数:
// this mimics the prefixing behavior of the eth_sign JSON-RPC method.
function prefixed(hash) {
return ethereumjs.ABI.soliditySHA3(
["string", "bytes32"],
["\x19Ethereum Signed Message:\n32", hash]
);
}
function recoverSigner(message, signature) {
var split = ethereumjs.Util.fromRpcSig(signature);
var publicKey = ethereumjs.Util.ecrecover(message, split.v, split.r, split.s);
var signer = ethereumjs.Util.pubToAddress(publicKey).toString("hex");
return signer;
}
function isValidSignature(contractAddress, amount, signature, expectedSigner) {
var message = prefixed(constructPaymentMessage(contractAddress, amount));
var signer = recoverSigner(message, signature);
return signer.toLowerCase() ==
ethereumjs.Util.stripHexPrefix(expectedSigner).toLowerCase();
}
模块化合同
构建合约的模块化方法可帮助您降低复杂性并提高可读性,这将有助于在开发和代码审查期间识别错误和漏洞。
如果您单独指定和控制行为或每个模块,则您必须考虑的交互只是模块规范之间的交互,而不是合约的所有其他移动部分。
在下面的示例中,合约使用 Balances 库的 move 方法来检查地址之间发送的余额是否符合您的预期。
通过这种方式,余额库提供了一个独立的组件,可以正确跟踪账户余额。
很容易验证 Balances 库永远不会产生负余额或溢出,并且所有余额的总和在合约的整个生命周期内都是不变量。
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 uint256) storage balances, address from, address to, uint amount) internal {
require(balances[from] >= amount);
require(balances[to] + amount >= balances[to]);
balances[from] -= amount;
balances[to] += amount;
}
}
contract Token {
mapping(address => uint256) balances;
using Balances for *;
mapping(address => mapping (address => uint256)) allowed;
event Transfer(address from, address to, uint amount);
event Approval(address owner, address spender, uint amount);
function transfer(address to, uint amount) external returns (bool success) {
balances.move(msg.sender, to, amount);
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(address from, address to, uint amount) external returns (bool success) {
require(allowed[from][msg.sender] >= amount);
allowed[from][msg.sender] -= amount;
balances.move(from, to, amount);
emit Transfer(from, to, amount);
return true;
}
function approve(address spender, uint tokens) external returns (bool success) {
require(allowed[msg.sender][spender] == 0, "");
allowed[msg.sender][spender] = tokens;
emit Approval(msg.sender, spender, tokens);
return true;
}
function balanceOf(address tokenOwner) external view returns (uint balance) {
return balances[tokenOwner];
}
}
参考资料
https://docs.soliditylang.org/en/latest/solidity-by-example.html