基础入门智能合约简单的智能合约区块链基础以太坊虚拟机安装Solidity编译器版本Remixnpm / Node.jsDocker根据例子学习Solidity投票合约秘密竞价(盲拍)合约安全的远程购买合约SOLIDITY 详解Solidity 源文件结构版权许可(SPDX License Identifier)Pragmas版本标识ABI Coder PragmaSMTChecker导入其他源文件注释合约结构状态变量函数函数修改器(modifier)事件(Event)
基础
入门智能合约
简单的智能合约
存储合约
把一个数据保存到链上
// SPDX-License-Identifier: GPL-3.0 // 源代码版本许可 pragma solidity >=0.4.16 <0.9.0; // 指定编译源代码的适用版本 contract SimpleStorage { uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }
货币合约(Subcurrency)
下面的合约实现了一个最简单的加密货币。
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract Coin { // address 类型是一个160位的值,且不允许任何算数操作,适合存储合约地址或外部人员的密钥对。 // public 自动生成一个getter函数,允许你在这个合约之外访问这个状态变量的当前值。 address public minter; mapping (address => uint) public balances; // 使用event声明了事件,轻客户端可以通过事件针对变化作出高效的反应 event Sent(address from, address to, uint amount); // 这是构造函数,只有当合约创建时运行 constructor() { minter = msg.sender; } function mint(address receiver, uint amount) public { require(msg.sender == minter); require(amount < 1e60); balances[receiver] += amount; } function send(address receiver, uint amount) public { require(amount <= balances[msg.sender], "Insufficient balance."); balances[msg.sender] -= amount; balances[receiver] += amount; emit Sent(msg.sender, receiver, amount); } }
上面代码中minter字段由public关键字自动创建的getter函数如下:
function minter() external view returns (address) { return minter; }
虽然不会显式存在,但是你不能再定义一个和上面完全一样的函数了。
而balances由public关键字自动创建的getter函数如下:
function balances(address _account) external view returns (uint) { return balances[_account]; }
声明的event在send最后一行被触发。用户界面(当然也包括服务器应用程序)可以监听区块链上正在发送的事件,而不会花费太多成本。一旦它被发出,监听该事件的listener都将收到通知。为了监听这个事件,你可以使用如下JavaScript代码(假设 Coin 是已经通过 web3.js 创建好的合约对象 ):
Coin.Sent().watch({}, '', function(error, result) { if (!error) { console.log("Coin transfer: " + result.args.amount + " coins were sent from " + result.args.from + " to " + result.args.to + "."); console.log("Balances now:\n" + "Sender: " + Coin.balances.call(result.args.from) + "Receiver: " + Coin.balances.call(result.args.to)); } })
区块链基础
交易/事务
区块链是全球共享的事务性数据库。必须创建一个被所有其他人所接受的事务才能改变数据库中的东西。事务一词意味着你的操作要么一点没做,要么全部完成。
区块
比特币中的交易会打包到区块中,然后分发给所有参与节点。这些块按时间形成了一个线性序列,这正是“区块链”这个词的来源。
区块以一定的时间间隔添加到链上 —— 对于以太坊,这间隔大约是17秒。
如果两笔交易互相矛盾,那么最终被确认为后发生的交易将被拒绝,不会被包含到区块中。这即是“双花攻击 (double-spend attack)”。
以太坊虚拟机
概述
以太坊虚拟机 EVM 是智能合约的运行环境。它不仅是沙盒封装的,而且是完全隔离的,也就是说在 EVM 中运行代码是无法访问网络、文件系统和其他进程的。甚至智能合约之间的访问也是受限的。
账户
以太坊中有两类账户(它们共用同一个地址空间):
- 外部账户:由公钥-私钥对(也就是人)控制,地址由公钥决定的
- 合约账户:由和账户一起存储的代码控制,地址是在创建该合约时确定的(这个地址通过合约创建者的地址和从该地址发出过的交易数量计算得到的,也就是所谓的“nonce”)
交易
交易可以看作是从一个帐户发送到另一个帐户的消息。
如果目标账户是零账户(账户地址为
0
),此交易将创建一个 新合约。在合约创建的过程中,它的代码还是空的。所以直到构造函数执行结束,你都不应该在其中调用合约自己函数。
Gas
发送者账户需要预付的手续费=
gas_price * gas
。一旦 gas 被耗尽(比如降为负值),将会触发一个 out-of-gas 异常。当前调用帧(call frame)所做的所有状态修改都将被回滚。
存储,内存和栈
每个账户有一块持久化内存区称为 存储 。 存储是将256位字映射到256位字的键值存储区。 在合约中枚举存储是不可能的,且读存储的相对开销很高,修改存储的开销甚至更高。合约只能读写存储区内属于自己的部分。
第二个内存区称为 内存 ,合约会试图为每一次消息调用获取一块被重新擦拭干净的内存实例。 内存是线性的,可按字节级寻址,但读的长度被限制为256位,而写的长度可以是8位或256位。当访问(无论是读还是写)之前从未访问过的内存字(word)时(无论是偏移到该字内的任何位置),内存将按字进行扩展(每个字是256位)。扩容也将消耗一定的gas。 随着内存使用量的增长,其费用也会增高(以平方级别)。
EVM 不是基于寄存器的,而是基于栈的,因此所有的计算都在一个被称为 栈(stack) 的区域执行。 栈最大有1024个元素,每个元素长度是一个字(256位)。对栈的访问只限于其顶端,限制方式为:允许拷贝最顶端的16个元素中的一个到栈顶,或者是交换栈顶元素和下面16个元素中的一个。所有其他操作都只能取最顶的两个(或一个,或更多,取决于具体的操作)元素,运算后,把结果压入栈顶。当然可以把栈上的元素放到存储或内存中。但是无法只访问栈上指定深度的那个元素,除非先从栈顶移除其他元素。
指令集
EVM的指令集量应尽量少,以最大限度地避免可能导致共识问题的错误实现。所有的指令都是针对”256位的字(word)”这个基本的数据类型来进行操作。具备常用的算术、位、逻辑和比较操作。也可以做到有条件和无条件跳转。此外,合约可以访问当前区块的相关属性,比如它的编号和时间戳。
消息调用
合约可以通过消息调用的方式来调用其它合约或者发送以太币到非合约账户。
委托调用(delegatecall)
目标地址的代码将在发起调用的合约的上下文中执行,并且
msg.sender
和 msg.value
不变。 日志
一路映射到区块层级的存储数据,可从区块链外高效访问。Solidity用它来实现事件(events)。
合约创建
负载的代码被执行并存储为合约代码,调用者/创建者在栈上得到新合约的地址。
失效和自毁
合约可以调用
selfdestruct
执行自毁操作。安装Solidity编译器
版本
版本迭代变更较大,推荐使用最新版本。
Remix
如果处理大型合约或者需要更多的编译选项,推荐使用命令行编译器solc。
npm / Node.js
可以使用
npm
便捷地安装Solidity编译器solcjs,但它没有solc那么全的功能。安装命令如下:# 注意命令符为solcjs而非solc npm install -g solc
Docker
使用docker构建编译器
docker run ethereum/solc:stable solc --version
目前,docker 镜像只含有 solc 的可执行程序,因此你需要额外的工作去把源代码和输出目录连接起来。
根据例子学习Solidity
投票合约
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; /// @title 委托投票 contract Ballot { // 这里声明了一个新的复合类型用于稍后的变量 // 它用来表示一个选民 struct Voter { uint weight; // 计票的权重 bool voted; // 若为真,代表该人已投票 address delegate; // 被委托人 uint vote; // 投票提案的索引 } // 提案的类型 struct Proposal { bytes32 name; // 简称(最长32个字节) uint voteCount; // 得票数 } address public chairperson; // 这声明了一个状态变量,为每个可能的地址存储一个 `Voter`。 mapping(address => Voter) public voters; // 一个 `Proposal` 结构类型的动态数组 Proposal[] public proposals; /// 为 `proposalNames` 中的每个提案,创建一个新的(投票)表决 constructor(bytes32[] memory proposalNames) { chairperson = msg.sender; voters[chairperson].weight = 1; //对于提供的每个提案名称, //创建一个新的 Proposal 对象并把它添加到数组的末尾。 for (uint i = 0; i < proposalNames.length; i++) { // `Proposal({...})` 创建一个临时 Proposal 对象, // `proposals.push(...)` 将其添加到 `proposals` 的末尾 proposals.push(Proposal({ name: proposalNames[i], voteCount: 0 })); } } // 授权 `voter` 对这个(投票)表决进行投票 // 只有 `chairperson` 可以调用该函数。 function giveRightToVote(address voter) public { // 若 `require` 的第一个参数的计算结果为 `false`, // 则终止执行,撤销所有对状态和以太币余额的改动。 // 在旧版的 EVM 中这曾经会消耗所有 gas,但现在不会了。 // 使用 require 来检查函数是否被正确地调用,是一个好习惯。 // 你也可以在 require 的第二个参数中提供一个对错误情况的解释。 require( msg.sender == chairperson, "Only chairperson can give right to vote." ); require( !voters[voter].voted, "The voter already voted." ); require(voters[voter].weight == 0); voters[voter].weight = 1; } /// 把你的投票委托到投票者 `to`。 function delegate(address to) public { // 传引用 Voter storage sender = voters[msg.sender]; require(!sender.voted, "You already voted."); require(to != msg.sender, "Self-delegation is disallowed."); // 委托是可以传递的,只要被委托者 `to` 也设置了委托。 // 一般来说,这种循环委托是危险的。因为,如果传递的链条太长, // 则可能需消耗的gas要多于区块中剩余的(大于区块设置的gasLimit), // 这种情况下,委托不会被执行。 // 而在另一些情况下,如果形成闭环,则会让合约完全卡住。 while (voters[to].delegate != address(0)) { to = voters[to].delegate; // 不允许闭环委托 require(to != msg.sender, "Found loop in delegation."); } // `sender` 是一个引用, 相当于对 `voters[msg.sender].voted` 进行修改 sender.voted = true; sender.delegate = to; Voter storage delegate_ = voters[to]; if (delegate_.voted) { // 若被委托者已经投过票了,直接增加得票数 proposals[delegate_.vote].voteCount += sender.weight; } else { // 若被委托者还没投票,增加委托者的权重 delegate_.weight += sender.weight; } } /// 把你的票(包括委托给你的票), /// 投给提案 `proposals[proposal].name`. function vote(uint proposal) public { Voter storage sender = voters[msg.sender]; require(!sender.voted, "Already voted."); sender.voted = true; sender.vote = proposal; // 如果 `proposal` 超过了数组的范围,则会自动抛出异常,并恢复所有的改动 proposals[proposal].voteCount += sender.weight; } /// @dev 结合之前所有的投票,计算出最终胜出的提案 function winningProposal() public view returns (uint winningProposal_) { uint winningVoteCount = 0; for (uint p = 0; p < proposals.length; p++) { if (proposals[p].voteCount > winningVoteCount) { winningVoteCount = proposals[p].voteCount; winningProposal_ = p; } } } // 调用 winningProposal() 函数以获取提案数组中获胜者的索引,并以此返回获胜者的名称 function winnerName() public view returns (bytes32 winnerName_) { winnerName_ = proposals[winningProposal()].name; } }
秘密竞价(盲拍)合约
简单的公开拍卖
// SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.7.0; contract SimpleAuction { // 拍卖的参数。 address payable public beneficiary; // 时间是unix的绝对时间戳(自1970-01-01以来的秒数) // 或以秒为单位的时间段。 uint public auctionEnd; // 拍卖的当前状态 address public highestBidder; uint public highestBid; //可以取回的之前的出价 mapping(address => uint) pendingReturns; // 拍卖结束后设为 true,将禁止所有的变更 bool ended; // 变更触发的事件 event HighestBidIncreased(address bidder, uint amount); event AuctionEnded(address winner, uint amount); // 以下是所谓的 natspec 注释,可以通过三个斜杠来识别。 // 当用户被要求确认交易时将显示。 /// 以受益者地址 `_beneficiary` 的名义, /// 创建一个简单的拍卖,拍卖时间为 `_biddingTime` 秒。 constructor( uint _biddingTime, address payable _beneficiary ) { beneficiary = _beneficiary; auctionEnd = block.timestamp + _biddingTime; } /// 对拍卖进行出价,具体的出价随交易一起发送。 /// 如果没有在拍卖中胜出,则返还出价。 function bid() public payable { // 参数不是必要的。因为所有的信息已经包含在了交易中。 // 对于能接收以太币的函数,关键字 payable 是必须的。 // 如果拍卖已结束,撤销函数的调用。 require( block.timestamp <= auctionEnd, "Auction already ended." ); // 如果出价不够高,返还你的钱 require( msg.value > highestBid, "There already is a higher bid." ); if (highestBid != 0) { // 返还出价时,简单地直接调用 highestBidder.send(highestBid) 函数, // 是有安全风险的,因为它有可能执行一个非信任合约。 // 更为安全的做法是让接收方自己提取金钱。 pendingReturns[highestBidder] += highestBid; } highestBidder = msg.sender; highestBid = msg.value; emit HighestBidIncreased(msg.sender, msg.value); } /// 取回出价(当该出价已被超越) function withdraw() public returns (bool) { uint amount = pendingReturns[msg.sender]; if (amount > 0) { // 这里很重要,首先要设零值。 // 因为,作为接收调用的一部分, // 接收者可以在 `send` 返回之前,重新调用该函数。 pendingReturns[msg.sender] = 0; if (!payable(msg.sender).send(amount)) { // 这里不需抛出异常,只需重置未付款 pendingReturns[msg.sender] = amount; return false; } } return true; } /// 结束拍卖,并把最高的出价发送给受益人 function auctionEnd() public { // 对于可与其他合约交互的函数(意味着它会调用其他函数或发送以太币), // 一个好的指导方针是将其结构分为三个阶段: // 1. 检查条件 // 2. 执行动作 (可能会改变条件) // 3. 与其他合约交互 // 如果这些阶段相混合,其他的合约可能会回调当前合约并修改状态, // 或者导致某些效果(比如支付以太币)多次生效。 // 如果合约内调用的函数包含了与外部合约的交互, // 则它也会被认为是与外部合约有交互的。 // 1. 条件 require(block.timestamp >= auctionEnd, "Auction not yet ended."); require(!ended, "auctionEnd has already been called."); // 2. 生效 ended = true; emit AuctionEnded(highestBidder, highestBid); // 3. 交互 beneficiary.transfer(highestBid); } }
秘密竞拍(盲拍)
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; 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); /// 使用 modifier 可以更便捷的校验函数的入参。 /// `onlyBefore` 会被用于后面的 `bid` 函数: /// 新的函数体是由 modifier 本身的函数体,并用原函数体替换 `_;` 语句来组成的。 modifier onlyBefore(uint _time) { require(block.timestamp < _time); _; } modifier onlyAfter(uint _time) { require(block.timestamp > _time); _; } constructor( uint _biddingTime, uint _revealTime, address payable _beneficiary ) public { beneficiary = _beneficiary; biddingEnd = block.timestamp + _biddingTime; revealEnd = biddingEnd + _revealTime; } /// 可以通过 `_blindedBid` = keccak256(value, fake, secret) /// 设置一个秘密竞拍。 /// 只有在出价披露阶段被正确披露,已发送的以太币才会被退还。 /// 如果与出价一起发送的以太币至少为 “value” 且 “fake” 不为真,则出价有效。 /// 将 “fake” 设置为 true ,然后发送满足订金金额但又不与出价相同的金额是隐藏实际出价的方法。 /// 同一个地址可以放置多个出价。 function bid(bytes32 _blindedBid) public payable onlyBefore(biddingEnd) { bids[msg.sender].push(Bid({ blindedBid: _blindedBid, deposit: msg.value })); } /// 披露你的秘密竞拍出价。 /// 对于所有正确披露的无效出价以及除最高出价以外的所有出价,你都将获得退款。 function reveal( uint[] _values, bool[] _fake, bytes32[] _secret ) public onlyAfter(biddingEnd) onlyBefore(revealEnd) { uint length = bids[msg.sender].length; require(_values.length == length); require(_fake.length == length); require(_secret.length == length); uint refund; for (uint i = 0; i < length; i++) { Bid storage bid = bids[msg.sender][i]; (uint value, bool fake, bytes32 secret) = (_values[i], _fake[i], _secret[i]); if (bid.blindedBid != keccak256(value, fake, secret)) { // 出价未能正确披露 // 不返还订金 continue; } refund += bid.deposit; if (!fake && bid.deposit >= value) { if (placeBid(msg.sender, value)) refund -= value; } // 使发送者不可能再次认领同一笔订金 bid.blindedBid = bytes32(0); } msg.sender.transfer(refund); } // 这是一个 "internal" 函数, 意味着它只能在本合约(或继承合约)内被调用 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; } /// 取回出价(当该出价已被超越) function withdraw() public { uint amount = pendingReturns[msg.sender]; if (amount > 0) { // 这里很重要,首先要设零值。 // 因为,作为接收调用的一部分, // 接收者可以在 `transfer` 返回之前重新调用该函数。(可查看上面关于‘条件 -> 影响 -> 交互’的标注) pendingReturns[msg.sender] = 0; msg.sender.transfer(amount); } } /// 结束拍卖,并把最高的出价发送给受益人 function auctionEnd() public onlyAfter(revealEnd) { require(!ended); emit AuctionEnded(highestBidder, highestBid); ended = true; beneficiary.transfer(highestBid); } }
安全的远程购买合约
// SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract Purchase { uint public value; address payable public seller; address payable public buyer; enum State { Created, Locked, Release, Inactive } State public state; modifier condition(bool _condition) { require(_condition); _; } modifier onlyBuyer() { require( msg.sender == buyer, "Only buyer can call this." ); _; } modifier onlySeller() { require( msg.sender == seller, "Only seller can call this." ); _; } modifier inState(State _state) { require( state == _state, "Invalid state." ); _; } event Aborted(); event PurchaseConfirmed(); event ItemReceived(); event SellerRefunded(); //确保 `msg.value` 是一个偶数。 //如果它是一个奇数,则它将被截断。 //通过乘法检查它不是奇数。 constructor() payable { seller = payable(msg.sender); value = msg.value / 2; require((2 * value) == msg.value, "Value has to be even."); } ///中止购买并回收以太币。 ///只能在合约被锁定之前由卖家调用。 function abort() public onlySeller inState(State.Created) { emit Aborted(); state = State.Inactive; seller.transfer(address(this).balance); } /// 买家确认购买。 /// 交易必须包含 `2 * value` 个以太币。 /// 以太币会被锁定,直到 confirmReceived 被调用。 function confirmPurchase() public inState(State.Created) condition(msg.value == (2 * value)) payable { emit PurchaseConfirmed(); buyer = payable(msg.sender); state = State.Locked; } /// 确认你(买家)已经收到商品。 /// 这会释放被锁定的以太币。 function confirmReceived() public onlyBuyer inState(State.Locked) { emit ItemReceived(); // It is important to change the state first because // otherwise, the contracts called using `send` below // can call in again here. state = State.Release; buyer.transfer(value); } /// This function refunds the seller, i.e. /// pays back the locked funds of the seller. function refundSeller() public onlySeller inState(State.Release) { emit SellerRefunded(); // It is important to change the state first because // otherwise, the contracts called using `send` below // can call in again here. state = State.Inactive; seller.transfer(3 * value); } } }
SOLIDITY 详解
Solidity 源文件结构
版权许可(SPDX License Identifier)
版权指定为MIT写法如下:
// SPDX-License-Identifier: MIT
如果不想明确指定版权,可以按下面这样写:
// SPDX-License-Identifier: UNLICENSED
Pragmas
关键字
pragma
用来启用某些编译器检查。版本标识
pragma solidity ^0.5.2;
表示源文件适用版本不允许低于0.5.2,也不允许高于0.6.0。
ABI Coder Pragma
到目前为止,ABI Coder一共有两个版本,ABI coder (v1) 和 ABI coder (v2)。
Solidity 0.7.4 以前默认就是v1
Solidity 0.7.4 之后可以使用
pragma experimental ABIEncoderV2
选用v2Solidity 0.7.4 之后默认就是v2,当然也可以使用
pragma abicoder v1;
选用v1SMTChecker
SMT(Satisfiability modulo theories)可以进行代码安全检查。使用
pragma experimental SMTChecker;
, 就可以获得 SMT solver 额外的安全检查。但是这个模块目前不支持 Solidity 的全部语法特性,因此有可能输出一些警告信息。导入其他源文件
语法与语义
一般情况写法如下:
import "filename";
如果要修改符号名则写法如下:
import * as symbolName from "filename"; // 或 import "filename" as symbolName;
如果修改某个别符号,写法如下:
import {symbol1 as alias, symbol2} from "filename";
路径
一般使用相对路径,如果要引入当前源文件同目录下的文件,则写法如下:
import "./filename" as symbolName;
通常,目录层次不必严格映射到本地文件系统, 它也可以映射到能通过诸如 ipfs,http 或者 git 发现的资源。
在实际的编译器中使用
通过将指定路径前缀重映射到另一个路径。
例如,
github.com/ethereum/dapp-bin/library
会被重映射到 /usr/local/dapp-bin/library
, 此时编译器将从重映射位置读取文件。如果重映射到多个路径,优先尝试重映射路径最长的一个。注释
单行注释://
多行注释:/*...*/
natspec 注释:/// 或 /**...*/,一般直接用在函数声明或语句使用上。
合约结构
状态变量
pragma solidity >=0.4.0 <0.9.0; contract TinyStorage { uint storedXlbData; // 状态变量 }
函数
函数是代码的可执行单元。函数通常在合约内部定义,但也可以在合约外定义。
// SPDX-License-Identifier: GPL-3.0 pragma solidity >0.7.0 <0.9.0; contract TinyAuction { function Mybid() public payable { // 定义函数 // ... } } // Helper function defined outside of a contract function helper(uint x) pure returns (uint) { return x * 2; }
函数修改器(modifier)
pragma solidity >=0.4.22 <0.9.0; contract MyPurchase { address public seller; modifier onlySeller() { // 修改器 require( msg.sender == seller, "Only seller can call this." ); _; } function abort() public onlySeller { // 修改器用法 // ... } }
事件(Event)
事件是能方便地调用以太坊虚拟机日志功能的接口。
pragma solidity >=0.4.21 <0.9.0; contract TinyAuction { event HighestBidIncreased(address bidder, uint amount); // 事件 function bid() public payable { // ... emit HighestBidIncreased(msg.sender, msg.value); // 触发事件 } }