以下内容还是从 Solidity 文档里摘出来的。

智能合约入门/介绍

第一个基本的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.0;

contract SimpleStorage {
uint storedData;

function set(uint x) public {
storedData = x;
}

function get() public constant returns (uint) {
return storedData;
}
}

一个 contract 可以被认为是一个类型。
默认的 unint 就是256位的。
storedData 可以被认为是 state variable,状态变量。在 Solidity 的概念里面,这个东西可以被认为是数据库里面的一个槽,可以被函数查询和修改。注意看它不是 public 的,所以没有合成方法。

访问状态变量不需要用 this前缀(在什么 scope 下都不需要吗?)。

这个例子没有限制任何其他人调用修改状态变量的方法。

子货币的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
pragma solidity ^0.4.0;

contract Coin {
// The keyword "public" makes those variables
// readable from outside.
address public minter;
mapping (address => uint) public balances;

// Events allow light clients to react on
// changes efficiently.
event Sent(address from, address to, uint amount);

// This is the constructor whose code is
// run only when the contract is created.
function Coin() public {
minter = msg.sender;
}

function mint(address receiver, uint amount) public {
if (msg.sender != minter) return;
balances[receiver] += amount;
}

function send(address receiver, uint amount) public {
if (balances[msg.sender] < amount) return;
balances[msg.sender] -= amount;
balances[receiver] += amount;
Sent(msg.sender, receiver, amount);
}
}

address 是160位的类型(问:为什么不是2的整数次幂?如何)。可以用来存储合约地址或者属于外部人的钥匙对(所谓的 msg.sender?)。public 关键字会让编译器自动帮忙生成一个访问器函数:

1
function minter() returns (address) { return minter; }

mapping (address => uint) public balances创造了一个公共状态变量,用 hash 表的形式来把地址映射到整数,其实就是把户头映射到金钱余额。这一个公共变量被虚拟地初始化了,所以所有合法的key都存在,而它们映射value都是零值(对整数而言,当然应该是0)。因为这个无限大的 map 的存在,所以不可能遍历所有的 key,也不能遍历所有的 value,只能依赖于外部数组来记住有意义的 key。而编译器帮忙生成的函数,则看起来是这个样子的:
1
2
3
function balances(address _account) public view returns (uint) {
return balances[_account];
}

也就是说,调用的时候,用类 balances(0x1234567)的方式来获取账户余额?

Event 的定义和我们习惯的就很相似了。但原文中举了一个观察的例子,不知道是在 Web3 api 里面使用,还是在智能合约里面使用:

1
2
3
4
5
6
7
8
9
10
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));
}
})

当然这个例子也提供了对 balances 函数的调用示例。

Coin 函数就是在创建合约的时候只执行一次不能再被执行的。这个构造器其实显式地把创建合约的 msg(而不只是 sender)以及 tx 和 block 这样的默认变量都保存下来了。对于所有函数而言,msg.sender 永远指向当前这个函数的调用地址。

mint 因为设置了卫语句,所以在不是创建者的调用面前会提前返回。而 send 则可以被任何人调用。

这些转账事件当然不可能被链自带的货币系统反映出来。但因为这个合约自己带了事件,所以可以开发针对它的区块链浏览器。

区块链基础

事务

区块链是全局共享的事务型数据库,以太坊甚至可以被认为是一个序列化隔离级别的数据库。所有加入网络的人都可以从这个数据库里读取条目。如果你想改变数据库里的什么👻东西,你创造的交易必须被所有人接受。事务,也就意味着操作是原子化的,事务里的所有操作要么都被完成,要么都没执行。此外,没有人能够篡改已经被应用数据库里的事务。

只有签过名的事务,才能做相应的修改—用密码学和权限隔离来保证安全性。

区块

一个需要克服的主要障碍是,按照比特币的说法,是所谓的“双花攻击”。也就是如果网络中存在相互矛盾的两笔事务怎么办?

抽象的回答是,你不用担心这个。一个事务的排序总会为你挑选出来,(被选中的)事务会被打包进一个所谓的“区块”里,然后他们会被执行和分发到所有参与节点里面。如果两个合约相互矛盾,那么被排在第二位的合约会被拒绝而不成为区块的一部分。

这些区块行程一个线性序列,区块链这个词就源于此。

作为“顺序挑选机制”(即所谓的“挖矿”)的一部分,区块可能会被回滚,但这种情况只会发生在链的末梢。越多的区块被加到顶部,原来的区块就越不容易被回滚。

以太坊虚拟机

概览

以太坊虚拟机(EVM)是智能合约的运行时。它不只是一个沙盒,而且被完全隔离了,也就是说EVM 不能访问网络、文件系统和其他进程(Fabric 的智能合约理论上没有这个限制)。智能合约甚至受限访问其他智能合约。

账户

以太坊中有两类账户共享同样的地址空间:由公私钥对(即人类)控制的外部账户以及与合约一同存储的代码控制的合约账户。

外部账户的地址是又公钥控制的,而合约的地址则是在合约创建时决定的(由创建者的地址和从哪个地址里发出的事务数(即 nonce)衍生。)。

不考虑账户是不是存储有代码,两种类型被 EVM 平等对待。

每一个账户有持久化的的键值存储叫 storage,映射256位的字到256位的字。

除此之外,每个账户都有一个以太币(以 wei 为单位)余额,可以通过发送含有以太币的事务进行修改。

事务

事务是从一个账户发送往另一个账户(另一个账户可以是同一个账户或者特殊的零账户)的消息。它可以包含二进制数据(它的载荷)和以太币。

如果目标账户包含代码,那么代码可以被执行,而载荷则可以被当做输入数据(按,等同于 message call)。

如果目标账户是一个零账户(账户地址是0),事务会产生一个新合约(这也是编程式产生新合约的方法)。创造合约的时候,我们在事务里发送的载荷实际上并不是合约本身,而是能够产生合约的代码。

注意,这个零账户指的是一个 transaction 里面的 to 的零值:

1
2
3
4
5
6
7
8
transaction = {
nonce: '0x0',
gasLimit: '0x6acfc0', // 7000000
gasPrice: '0x4a817c800', // 20000000000
to: '0x0',
value: '0x0',
data: '0xfffff'
};

具体思辨内容见

Gas

一创建完成,每个事务总要收取一定量的 gas,用意是为了限制执行事务所需的工作量,并为执行事务付费。当 EVM 执行事务的时候,gas 根据特定规则被逐渐消耗。

gas 加个是由事务创建者设立的,发出(事务的)账户必须付出gas_price * gas的预付款。如果执行之后还有 gas 剩余,它会被原路退回。

如果 gas 在任何时刻被用尽,一个 out-of-gas 异常会被触发,因此会反转当前调用帧对状态的所有修改。

Storage, Memory 和 Stack

每个账户都有一个持久化的内存区域,被称作storage。Storage 是一个键值存储,它把256位的字映射到256位的字。不可能在一个合约内部枚举 storage,而读storage也是相对昂贵的, 修改 storage 更是昂贵。一个合约不能读和写它自己的 storage 中自己拥有的部分之外的东西。

第二个内存区域叫做memory,它让合约为每一个消息调用获得了一个全新清理过的实例。memory 是线性的,可以在字节级别被寻址,但读被限制到256位宽,写得可以写8位到256位宽。memory 被字(256位)展开,当访问(不管是读或者写)一个之前没有接触过的内存字(即,一个字内的任何偏移)。在展开的时候,gas 的花费也必须要之丰富。memory 增长的越大,越昂贵(因为它是平方级扩容的)。

EVM 不是个寄存器机器而是个栈机器,所以所有的计算都在一个叫 stack 的区域上执行。栈有一个1024个元素的最大尺寸,并且包含256位的字。只允许通过这种方式从栈顶访问元素:可以拷贝最顶上的16个元素中的一个或者把最顶上的十六个元素中的一个与其下的元素翻转。按:这也就是 stack 里的局部变量表只允许有16个变量的原因。看起来所有的 EVM 语言,包括但不限于 Solidity,都受这个限制影响。其他操作则只能从栈上取最顶上的两个(或一或更多,取决于具体操作)元素并将结果推上栈。当然可能把栈上的元素移动到 storage 和 memory,但不可能在不移除栈顶元素的前提下,访问栈上更深的任意元素。

指令集

EVM 的指令集被保持极小规模,以防错误的实现引发共识问题。所有的指令都操作在基本数据类型,256位的字上。常见的算数、位、逻辑和比较操作符是现成的。条件和非条件跳转是可行的。除此之外,合约能够访问当前区块的相关属性比如数字和时间戳。

消息调用

合约可以调用其他合约,或者通过消息调用的方式发送以太币到非合约账户。消息调用类似事务,所以它有源、目标、数据载荷、以太币、gas 和返回值。事实上,所有的事务都包含一个顶级消息调用(按:可以理解为元消息调用),它可以创造更多的消息调用。

一个合约决定有多少它的剩余 gas 需要被伴随内部消息调用发送,有多少它想要保留。如果一个 out-of-gas 异常在内部调用发生了(或者任何其他异常),这件事会以一个被放在栈上的错误值作为信号。在这种情况下,只有伴随着这个调用的 gas 被用尽了(按:其它的 gas 没有被用尽)。在 Solidity 中,在这种情况下,调用方合约引发一个手动异常(按:下面传上来的是错误码,在这里才 raise 异常),所以异常状况就在调用栈上被冒泡上去了。

正如已经提到的,被调用的合约(它可以是调用者本身)会收到一个全新的被清理过的内存实例,并且可以访问调用载荷-它会在一个单独的被称作alldata 的区域里被提供。当它执行完成以后,它可以返回数据,数据会被存贮在调用者的预分配内存里的某一个部分。按:类似传统调用栈的返回值寄存器。调用被限制在一个1024的深度上,这意味着对更复杂的操作,循环应该优于递归。

Delegatecall(委托调用)/Callcode(调用代码) 和 Libraries(库)

存在消息调用的一个特殊变种,名为 delegatecall,它和一个消息调用完全一样,除了目标地址的代码是在调用方合约的上下文里执行(按:意即,不是在被调用合约上下文里执行),并且 msg.sender 和 msg.value 没有改变它们的值(按:和调用方合约里的值一样)。

这意味着一个合约可以在运行时动态地从其他地址提取代码。存储、当前地址和余额,依旧指向调用方合约,只有代码是从被调用方合约里拿来的。这意味着在 Solidity 中实现库这一特征是可能的:可复用的库代码可以应用到一个合约的存储里,意即,可以(通过复用库)实现复杂的数据结构。

日志

可以在专门索引过的数据结构里存储数据,这把所有的方法都映射到了区块级别(按:即在区块级别来思考怎么解决相关问题)。被称作log(日志)的特征被 Solidity 用来实现 events(事件)。合约不能在日志被创建后访问它们(即 log 被创建后就不能在内部从任何一个地方被读取了)。但它们可以从链外被读取。由于有一部分 log 数据被存储在布隆过滤器里,所以可以通过有效率和加密安全的方法来搜索其中数据,所以网络对等节点(轻客户端)不需要下载完整的区块就可以发行这些日志。

创建

合约甚至可以使用特殊的opcode(操作码)创建其他合约(它们并不只是简单地调用零合约地址,这已经是第二种已知的创建合约的方法了)。创建调用和普通消息调用的唯一差别是载荷数据被执行了,而结果被当做代码存储。调用者/创建者在栈上接收到新合约的地址。

自毁

代码被从区块链上移出的唯一可能性是当一个合约执行selfdestruct操作的时候。地址中存储的剩余以太币会被发送给一个设定好的目标(账户),接着状态(数据库的存储)中的存储和状态将被移除。

即使一个合约的代码中不包含一个到selfdestruct的调用,它仍然可以通过delegatecallcallcode来执行那个操作。

对于老以太坊节点而言,这个珊瑚并不一定是物理删除,也可以是软删除

当前外部账户(即个人的 account)是不能从 state 中移除掉的。按:即合约账户可以被移除

Solidity举例

选举

接下来的合约就非常复杂了,它显示了 Solidity 的一大堆优点。它实现了一个投票合约。当然,电子投票的主要问题是如何分配投票权给正确的人群和如何阻止操纵选举。我们不会在这里解决所有的问题,但最起码我们会显式被代理的选举如何完成,并且计票是自动和同时完全透明的。

奥妙就是每一个ballot(投票)一个合约,为每个选项提供一个短名字。然后合约的创建者作为主席会把投票权单独授予每个地址。

地址后面的人可以选择要么自己投票,要么把他们的投票权delegate(委托)一个他们信任的人。

在选举结束的时候,winningProposal()会返回最大投票数的建议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
pragma solidity ^0.4.16;

/// @title Voting with delegation.
contract Ballot {
// This declares a new complex type which will
// be used for variables later.
// It will represent a single voter.
struct Voter {
uint weight; // weight is accumulated by delegation
bool voted; // if true, that person already voted
address delegate; // person delegated to
uint vote; // index of the voted proposal
}

// This is a type for a single proposal.
struct Proposal {
bytes32 name; // short name (up to 32 bytes)
uint voteCount; // number of accumulated votes
}

address public chairperson;

// This declares a state variable that
// stores a `Voter` struct for each possible address.
mapping(address => Voter) public voters;

// A dynamically-sized array of `Proposal` structs.
Proposal[] public proposals;

/// Create a new ballot to choose one of `proposalNames`.
function Ballot(bytes32[] proposalNames) public {
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 < proposalNames.length; i++) {
// `Proposal({...})` creates a temporary
// Proposal object and `proposals.push(...)`
// appends it to the end of `proposals`.
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
}
}

// Give `voter` the right to vote on this ballot.
// May only be called by `chairperson`.
function giveRightToVote(address voter) public {
// If the argument of `require` evaluates to `false`,
// it terminates and reverts all changes to
// the state and to Ether balances. It is often
// a good idea to use this if functions are
// called incorrectly. But watch out, this
// will currently also consume all provided gas
// (this is planned to change in the future).
// require 类似断言,会反转所有的区块链 state,但还是会消耗 gas(这点未来会被修改),这比 return 好吗?
require((msg.sender == chairperson) && !voters[voter].voted && (voters[voter].weight == 0));
// 某个投票者地址获得了投票权
voters[voter].weight = 1;
}

/// Delegate your vote to the voter `to`.
function delegate(address to) public {
// assigns reference
// 第一个出现的显式 storage,这证明读成员变量赋给局部变量总是要求 storage 的
Voter storage sender = voters[msg.sender];
require(!sender.voted);

// Self-delegation is not allowed.
require(to != msg.sender);

// Forward the delegation as long as
// `to` also delegated.
// In general, such loops are very dangerous,
// because if they run too long, they might
// need more gas than is available in a block.
// In this case, the delegation will not be executed,
// but in other situations, such loops might
// cause a contract to get "stuck" completely.
// 这里使用了一个可以比较零地址的例子,证明不可以直接 != 0来比较
// 在现实中这样的循环可能很耗 gas
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;

// We found a loop in the delegation, not allowed.
// 内置 required 断言
require(to != msg.sender);
}

// Since `sender` is a reference, this
// modifies `voters[msg.sender].voted`
// 把被委托者的旧值作废掉
sender.voted = true;
sender.delegate = to;
Voter storage delegate = voters[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) public {
Voter storage sender = voters[msg.sender];
require(!sender.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)
{
// solidity 的 dummy 数值对象就是这样写的了
uint winningVoteCount = 0;
// 注意,这个 proposals 是数组,winner 其实只是这个数组的一个索引
for (uint p = 0; p < proposals.length; p++) {
if (proposals[p].voteCount > 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() public view
returns (bytes32 winnerName)
{
winnerName = proposals[winningProposal()].name;
}
}

这个合约可以提升的地方,就是怎样提升事务的效率?

盲拍卖

简单拍卖

接下来出现的简单明拍卖合约的大意是每个人都可以在投标期内把投标投出去。投标已经包含了发送金钱/以太币,以把投标者和投标绑定起来。如果最高投标上升了,前一个最高投标者会拿回她的钱。在投标周期的最后,合约要让受益人手动调用来获得它的钱,合约不能激活它自己。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
pragma solidity ^0.4.11;

contract SimpleAuction {
// 卖东西的人
// Parameters of the auction. Times are either
// absolute unix timestamps (seconds since 1970-01-01)
// or time periods in seconds.
address public beneficiary;
// 用整数来表示时间
uint public auctionEnd;

// 当前最高投标者的地址
// Current state of the auction.
address public highestBidder;
// 最高的投标
uint public highestBid;

// 受许可的之前投标的退款,在这里 withdrawal 等于 refund 了
// Allowed withdrawals of previous bids
mapping(address => uint) pendingReturns;

// Set to true at the end, disallows any change
bool ended;

// Events that will be fired on changes.
event HighestBidIncreased(address bidder, uint amount);
event AuctionEnded(address winner, uint amount);

// 三斜杠的是提示符评论,可以提示用户是否确认一个事务
// The following is a so-called natspec comment,
// recognizable by the three slashes.
// It will be shown when the user is asked to
// confirm a transaction.

/// Create a simple auction with `_biddingTime`
/// seconds bidding time on behalf of the
/// beneficiary address `_beneficiary`.
function SimpleAuction(
uint _biddingTime,
address _beneficiary
) public {
beneficiary = _beneficiary;
// 看来 now 也是秒数的意思。换言之,没有毫秒数。
auctionEnd = now + _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.
// payable 就是拿来修饰 function 的,所以它是 payble 的。
function bid() public 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.
require(now <= auctionEnd);

// If the bid is not higher, send the
// money back.
// 这个 payable 的合约也可以被断言所中断
require(msg.value > highestBid);

// 最高投标者的地址的另一种比对方法
if (highestBidder != 0) {
// Sending back the money by simply using
// highestBidder.send(highestBid) is a security risk
// because it could execute an untrusted contract.
// It is always safer to let the recipients
// withdraw their money themselves.
pendingReturns[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
HighestBidIncreased(msg.sender, msg.value);

// 这个方法没有任何直接摸合约 account 的地方,但这个payable 方法的内容里处处充满了 msg.value
}

/// Withdraw a bid that was overbid.
// 这个退款函数是谁来调用 ?
function withdraw() public returns (bool) {
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 `send` returns.
pendingReturns[msg.sender] = 0;

if (!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() public {
// 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
require(now >= auctionEnd); // auction did not yet end
require(!ended); // this function has already been called

// 2. Effects
ended = true;
AuctionEnded(highestBidder, highestBid);

// 3. Interaction
beneficiary.transfer(highestBid);
}
}

盲拍卖

盲拍卖的优点是没有向着拍卖终点的时间压力。

简化一个盲拍卖模型。用户先用头标志的 hash 投标。投标结束后大家揭示出自己的投标值,同时校验 hash 是否相同以及数值是否最高。

为了防止有些人拍完不给钱,这个合约还进一步要求大家把价值和投标一起发送,前者自然是明文的,而后者是加密的。为了不暴露隐私,合约接受一切附带的价值高于当前最高投标价的新投标转账。这也就意味着,在价值揭示环节,有些投标是无效的不成立的。投标者甚至可以用一些错误的高投标或者低投标来迷惑整个竞争。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
pragma solidity ^0.4.11;

contract BlindAuction {
struct Bid {
bytes32 blindedBid;
// 锁死存款
uint deposit;
}

address public beneficiary;
uint public biddingEnd;
uint public revealEnd;
bool public ended;

mapping(address => Bid[]) public bids;

address public highestBidder;
uint public highestBid;

// Allowed withdrawals of previous bids
mapping(address => uint) pendingReturns;

event AuctionEnded(address winner, uint highestBid);

/// 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 有点像装饰器,结合了 require,_指的就是被修饰方法的 body
modifier onlyBefore(uint _time) { require(now < _time); _; }
modifier onlyAfter(uint _time) { require(now > _time); _; }

function BlindAuction(
uint _biddingTime,
uint _revealTime,
address _beneficiary
) public {
beneficiary = _beneficiary;
// 在这里直接拿起点时间戳来生成接下来的时间节点就行了
biddingEnd = now + _biddingTime;
revealEnd = biddingEnd + _revealTime;
}

// keccak 函数是 SHA-3函数的别名,被以太坊更名过的函数。SHA-256实际上是 SHA-2 的一个子类型,keccak 算法是下一代的 SHA-3算法,是最强的散列算法。http://www.atool.org/hash.php
// keccak 是个松散参数函数,所有的输入都是散列的种子,会被函数直接拼接起来,类似分段签名相加的思路。
/// Place a blinded bid with `_blindedBid` = keccak256(value,
/// fake, secret).
/// The sent ether is only refunded if the bid is correctly
/// revealed in the revealing phase. The bid is valid if the
/// ether sent together with the bid is at least "value" and
/// "fake" is not true. Setting "fake" to true and sending
/// not the exact amount are ways to hide the real bid but
/// still make the required deposit. The same address can
/// place multiple bids.
// 一个地址可以做多个投标。只有被正确亮出的投标值才会被返还(其他的投标值就不返还了吗?)。只有附带的投标值大于等于value(这是什么意思?)而且 fake 不是 true。设置 fake 为 true 而且发送不准确的数额是隐藏真实投标但满足要求的锁定存款的方法。
function bid(bytes32 _blindedBid)
public
payable
onlyBefore(biddingEnd)
{
bids[msg.sender].push(Bid({
blindedBid: _blindedBid,
deposit: msg.value
}));
}

// 亮标。用户可以得到所有被正确掩盖的错标,和除了最高标以外的所有标。
/// 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(
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++) {
var bid = bids[msg.sender][i];
var (value, fake, secret) =
(_values[i], _fake[i], _secret[i]);
if (bid.blindedBid != keccak256(value, fake, secret)) {
// Bid was not actually revealed.
// Do not refund deposit.
continue;
}
refund += bid.deposit;
// deposit 是实际存入的锁定价值,value 是用来做散列种子的声称价值
if (!fake && bid.deposit >= value) {
if (placeBid(msg.sender, value))
refund -= value;
}
// 因为以太坊可以被认为是序列化的事务隔离级别的,所以在这里置零可以抗并发问题
// Make it impossible for the sender to re-claim
// the same deposit.
bid.blindedBid = bytes32(0);
}
msg.sender.transfer(refund);
}

// internal 的第一个用法,internal 函数类似 private,只能在合约内部使用了。
// 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)
{
// 不中标。所以在这里 place 就是放标的意思?
if (value <= highestBid) {
return false;
}
if (highestBidder != 0) {
// 先把之前的最高标放进退款名单里面
// Refund the previously highest bidder.
pendingReturns[highestBidder] += highestBid;
}
// 替换最高标
highestBid = value;
highestBidder = bidder;
return true;
}

/// Withdraw a bid that was overbid.
function withdraw() public {
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;

msg.sender.transfer(amount);
}
}

// 在拍卖结束的时候把投标款转走。
/// End the auction and send the highest bid
/// to the beneficiary.
function auctionEnd()
public
onlyAfter(revealEnd)
{
require(!ended);
AuctionEnded(highestBidder, highestBid);
ended = true;
beneficiary.transfer(highestBid);
}
}

到目前为止,每个合约只有一个 payable 方法。

安全的远程购买

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
pragma solidity ^0.4.11;

contract Purchase {
uint public value;
address public seller;
address public buyer;
enum State { Created, Locked, Inactive }
State public state;

// 确认价值是偶数(为什么?)。由卖方初始化合约。这是第一个构造函数也是 payable 的合约。初始化合约的 value,大致等于一个买卖的押金。
// Ensure that `msg.value` is an even number.
// Division will truncate if it is an odd number.
// Check via multiplication that it wasn't an odd number.
function Purchase() public payable {
seller = msg.sender;
value = msg.value / 2;
require((2 * value) == msg.value);
}

modifier condition(bool _condition) {
require(_condition);
_;
}

modifier onlyBuyer() {
require(msg.sender == buyer);
_;
}

modifier onlySeller() {
require(msg.sender == seller);
_;
}

modifier inState(State _state) {
require(state == _state);
_;
}

event Aborted();
event PurchaseConfirmed();
event ItemReceived();

/// Abort the purchase and reclaim the ether.
/// Can only be called by the seller before
/// the contract is locked.
function abort()
public
onlySeller
inState(State.Created)
{
Aborted();
state = State.Inactive;
// 这个 balance 就是合约自带的所有 balance 了,而不是我们自定义的 value 一类的成员变量。
seller.transfer(this.balance);
}

/// Confirm the purchase as buyer.
/// Transaction has to include `2 * value` ether.
/// The ether will be locked until confirmReceived
/// is called.
function confirmPurchase()
public
inState(State.Created)
condition(msg.value == (2 * value))
payable
{
// 发出购买锁定事件
PurchaseConfirmed();
buyer = msg.sender;
state = State.Locked;
}

/// Confirm that you (the buyer) received the item.
/// This will release the locked ether.
function confirmReceived()
public
onlyBuyer
inState(State.Locked)
{
// 发出事件
ItemReceived();
// It is important to change the state first because
// otherwise, the contracts called using `send` below
// can call in again here.
state = State.Inactive;

// NOTE: This actually allows both the buyer and the seller to
// block the refund - the withdraw pattern should be used.
// 把押金转给买家,把差额交给卖家。为什么要这样设计?为了避免空口无凭的卖家浪费合约价值?
buyer.transfer(value);
seller.transfer(this.balance);
}
}

这足以证明一个合约有好几个 payable 方法,允许多角色博弈了。
这个合约没有任何的 internal 函数。

深入Solidity

Solidity 源文件的轮廓

源文件可以包含任意多的合约定义。

版本 pragma

1
2
// 这个尖角号意味着不能用高于0.5.0的编译器编译。这个数字说明了合约的最小编译器编译器是0.4.0。换言之,要用高级编译器,要 bump up 这个数字。
pragma solidity ^0.4.0;

引入其他源文件

语法和语义

Solidity 用类似 JavaScript 的引入语法,但不支持“default export”。在全局层次,你可以用以下形式的引入语句:

1
import "filename";

这个语句把该文件名下所有的全局符号引入到当前的全局作用域中。
1
import * as symbolName from "filename";

这个语句制造了一个新的全局符号,类似名字空间,所有该文件名下的符号,都是这个新的全局符号的成员。它和import "filename" as symbolName;等价。
1
import {symbol1 as alias, symbol2} from "filename";

部分引用和别名机制。

路径

.和..的机制同 Unix 系统。

要引用同一个文件夹下的文件,用这样的语法mport "./x" as x;。如果你使用了import "x" as x;,一个全局的“include directory”里的同名文件夹会被引用。

重映射问题

太无聊,直接看文档吧。

注释问题

注意看下面的文档,展示了如何写注释,也展示了如何写多返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.0;

/** @title Shape calculator. */
contract shapeCalculator {
/** @dev Calculates a rectangle's surface and perimeter.
* @param w Width of the rectangle.
* @param h Height of the rectangle.
* @return s The calculated surface.
* @return p The calculated perimeter.
*/
function rectangle(uint w, uint h) returns (uint s, uint p) {
s = w * h;
p = 2 * (w + h);
}
}

合约的结构

合约和面向对象程序设计语言里面的 class 很相似。每个合约可以包含状态变量函数函数修饰符事件结构类型枚举类型的声明。

状态变量

状态变量会被持久化在合约存储(状态)里:

1
2
3
4
5
6
pragma solidity ^0.4.0;

contract SimpleStorage {
uint storedData; // State variable
// ...
}

函数

函数是代码的可执行单元。

1
2
3
4
5
6
7
pragma solidity ^0.4.0;

contract SimpleAuction {
function bid() public payable { // Function
// ...
}
}

函数调用可以在内部发生,也可以在外部发生,而且面对不同的其他合约可以拥有不同的可见性

函数修饰符

函数修饰符可以以声明的方式修饰函数的语义(见Function Modifiers部分)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.11;

contract Purchase {
address public seller;

// 总是配合 require 函数使用。还可以看看 revert 什么的函数。
modifier onlySeller() { // Modifier
require(msg.sender == seller);
_;
}

function abort() public onlySeller { // Modifier usage
// ...
}
}

事件

事件就是拿来和 EVM 日志设施打交道的方便接口。

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.0;

contract SimpleAuction {
event HighestBidIncreased(address bidder, uint amount); // Event

function bid() public payable {
// ...
HighestBidIncreased(msg.sender, msg.value); // Triggering event
}
}

Struct 类型

类似 C 语言,是基本类型的封装组合。

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.0;

contract Ballot {
struct Voter { // Struct
uint weight;
bool voted;
address delegate;
uint vote;
}
}

枚举类型

依然类似 Java。

1
2
3
4
5
pragma solidity ^0.4.0;

contract Purchase {
enum State { Created, Locked, Inactive } // Enum
}

类型

Solidity 是静态类型语言,这意味着不管是状态变量还是局部变量,每个变量的类型必须在运行时被指定好(至少是已知的,见Type Deduction)。Solidity 提供几个至关重要的类型,可以被组合成复合类型。

值类型

以下的类型总是被称作值类型,因为这些类型的变量总是会被传值。他们作为函数参数或者赋值使用的时候,总是会被拷贝。

布尔类型

bool:可能值为常量 true 或者 false。

逻辑操作符和常见的编程语言操作符类似,也支持短路操作

整型

int/uint:各种尺寸的有符号和无符号整型数。关键字uint8unint256以8位为步长,以及int8int256。相对地,unintintuint256int256的别名(按:即默认数据宽度就是最宽)。

除法总是会造成截断,除非两个操作符都是字面量(?)或者字面量表达式。

x << y等于x * 2**y,而x >> y等于x / 2**y。用负数作 y 可能会出运行时异常-这是因为不同编程语言的移位有向0和向负无穷移位的区别。

定点数

定点数在 Solidity 里还没被完全支持。它们可以被声明,但不能被拿来赋值或者取值。

fixed/ufixed表达的是不同尺寸的有符号和无符号定点数。关键字ufixedMxNfixedMxN里,M 代表这个类型可以使用的位数,而N代表的是有多少个小数位可被使用。M必须可被8整除,从8增长到256。N必须在0到80之间。相应地,ufixedfixedufixed128x19fixed128x19的别名。

浮点数(IEEE 754)的小数点位置是可变的,而定点数是不可变的。

地址类型

地址拥有20字节的数据,它也有成员,而且在0.5.0以前是所有合约的基类。在0.5.0之后,合约类型不再从地址类型里衍生出来,但可以被显式地转化为地址。

地址的成员:

  • balancetransfer
1
2
3
address x = 0x123;
address myAddress = this;
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

特别地,向合约地址调用 transfer 的时候,每个合约地址的 fallback 函数会被调用。

  • send 是 transfer 低级对应物。如果执行失败,transfer 抛出异常而 send 返回 false。

  • call, callcode and delegatecall

call 可以使用任意数量任意类型的参数和接口交互。

1
2
3
address nameReg = 0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2;
nameReg.call("register", "MyName");
nameReg.call(bytes4(keccak256("fun(uint256)")), a);

修改提供的 gas 数量的函数:

1
namReg.call.gas(1000000)("register", "MyName");

类似地,也可以在调用函数的时候转钱:
1
nameReg.call.value(1 ether)("register", "MyName");

无关顺序的修饰符使用:
1
nameReg.call.gas(1000000).value(1 ether)("register", "MyName");

delegatecall可以被用来调用某个特定合约上的地址,这个方法的用意就是用来调用其他合约上的库函数。它的早期版本(家园以前)callcode 同理,而且callcode 不能访问msg.sendermsg.valuecallcode 将在未来的版本被移除。

尽量应该使用 transfer 而不要使用低级 API,因为它们破坏了 Solidity 的类型安全。

.gas()选项对于三个方法都可用,但.value()选项不被delegatecall所支持。

因为所有的合约都集成了地址的成员,所以在合约里可以这样查余额this.balance

定长字节数组

bytes1bytes2bytes3,…,bytes32bytebytes1的别名。

这个类型也支持常见的比较、位操作符,它还支持更重要的索引操作符:如果 x 是个bytesI类型,x[k](0 <= k < I)返回第 k
个字节。

动态长度字节数组

bytes:动态长度字节数组,见Arrays。不是一个值类型(为什么要放这里?)。

string:动态utf-8编码字符串,见Arrays。不是一个值类型(为什么要放这里?)。

如果有任意长度数据的需求,应该优先使用bytesstring,否则尽量使用定长的数据数据类型,bytes1bytes32比较便宜。

地址字面量

能够通过地址测试的十六进制字面量(加上0x长度为42的字符串)如0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF是 地址类型。

有理数和整数字面量

常见整数字面量:69
十进位小数字面量:1.3
科学计数法:2e10-2e102e-102.5e1.

早期版本的有理数除法会导致截断,当前版本不会,5/2现在等于2.5了。

字符串字面量

单引号和双引号都可以包裹字符串字面量,如"foo"'bar'。他们没有 C 中的尾随0。"foo"代表三个字节而不是四个。他们可以被隐式地转换为各种字节数组(见上文),定长不定长的都可能,当然也可以转化为string

字符串字面量支持转义符,如\n\xNN\uNNNN\xNN取一个十六进制的值,并插入一个正确的字节。\uNNNN取一个 Unicode 码点,并且插入一个UTF-8序数。

十六进制字面量

hex开头,用单双引号括起来:hex"001122FF"

枚举

这里的枚举也是可以显式地与整型数互相转换的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.16;

contract test {
enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;

function setGoStraight() public {
choice = ActionChoices.GoStraight;
}

// Since enum types are not part of the ABI, the signature of "getChoice"
// will automatically be changed to "getChoice() returns (uint8)"
// for all matters external to Solidity. The integer type used is just
// large enough to hold all enum values, i.e. if you have more values,
// `uint16` will be used and so on.
function getChoice() public view returns (ActionChoices) {
return choice;
}

function getDefaultChoice() public pure returns (uint) {
return uint(defaultChoice);
}
}
函数类型

同函数式编程里面的一等类型函数差不多。

函数类型被分为两种:内部和外部函数。

内部函数只能在当前合约内被调用(更具体地说,在当前的代码单元内,也就包含了内部函数库和继承下来的函数),因为他们不能在当前合约的上下文之外被执行。调用一个内部函数的实现方式就是让控制流跳到一个条目标签上。

外部函数包括一个地址和一个函数签名,他们可以被传递进/传递出外部函数调用。

函数类型的标记法是:

1
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]

不允许出现空的 return 语句。

默认函数都是 internal 的,所以 internal 实际上可以被省略(类似 C 语言呢)。

delete一个函数后,再调用它会出现运行时异常。

如果一个外部函数变量被 Solidity 之外的上下文调用(跨语言互操作 interoperability问题),他们将被当做function类型,它在一个bytes24类型里放了一个编码的地址,后面还跟着函数识别符。

public(或外部)函数同样包含一个叫selector的成员,它返回一个 ABI function selector

1
2
3
4
5
6
7
pragma solidity ^0.4.16;

contract Selector {
function f() public view returns (bytes4) {
return this.f.selector;
}
}

内部函数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
pragma solidity ^0.4.16;

library ArrayUtils {
// 同在一个上下文里的内部函数调用。
// internal functions can be used in internal library functions because
// they will be part of the same code context
function map(uint[] memory self, function (uint) pure returns (uint) f)
internal
pure
returns (uint[] memory r)
{
r = new uint[](self.length);
for (uint i = 0; i < self.length; i++) {
r[i] = f(self[i]);
}
}
function reduce(
uint[] memory self,
function (uint, uint) pure returns (uint) f
)
internal
pure
returns (uint r)
{
r = self[0];
for (uint i = 1; i < self.length; i++) {
r = f(r, self[i]);
}
}
function range(uint length) internal pure returns (uint[] memory r) {
r = new uint[](length);
for (uint i = 0; i < r.length; i++) {
r[i] = i;
}
}
}

contract Pyramid {
using ArrayUtils for *;
function pyramid(uint l) public pure returns (uint) {
return ArrayUtils.range(l).map(square).reduce(sum);
}
function square(uint x) internal pure returns (uint) {
return x * x;
}
function sum(uint x, uint y) internal pure returns (uint) {
return x + y;
}
}

一个把外部函数类型传递进 struct 的实例,这其实也是个预言机使用的好例子了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pragma solidity ^0.4.20; // should actually be 0.4.21

contract Oracle {
struct Request {
bytes data;
function(bytes memory) external callback;
}
Request[] requests;
event NewRequest(uint);
function query(bytes data, function(bytes memory) external callback) public {
requests.push(Request(data, callback));
emit NewRequest(requests.length - 1);
}
function reply(uint requestID, bytes response) public {
// Here goes the check that the reply comes from a trusted source
requests[requestID].callback(response);
}
}

contract OracleUser {
Oracle constant oracle = Oracle(0x1234567); // known contract
function buySomething() {
oracle.query("USD", this.oracleResponse);
}
function oracleResponse(bytes response) public {
require(msg.sender == address(oracle));
// Use the data
}
}

现在还不支持 Lambda 函数和内联函数。

引用类型

复合类型,也就是类型不衬进256位的类型,总要被比我们已经看到的值类型更仔细地对待。因为拷贝它们是非常昂贵的,所以我们要仔细考虑它们应该被存储在memory里(不会被持久化)还是被存储在storage(状态变量就放在这里)里。

数据位置

每个复合类型,数组和结构体,都有一个额外的注解,即“数据位置”,关于它是被存储在memory里还是在storage里。依据上下文的不同,总有一个缺省值,但它可以被往类型上添加storagememory覆盖掉。默认的函数参数是memory的,默认的局部变量是storage的(即这两者是可变的),状态变量的位置被强制设为storage

还有第三种数据位置,calldata,它是不可修改的,不持久化的,函数实参存储在里面。外部函数的函数参数(而不是返回参数)被强制为calldata,行为表现与memory很相似。

数据位置是重要的,因为他们改变了赋值行为:memorystorage之间的赋值、状态变量之间的赋值总是会产生独立拷贝。向局部storage 变量赋值,尽管只是赋予一个引用,这个引用总是指向一个状态变量,即使后者会改变。memorymemory之间的赋值不会产生拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pragma solidity ^0.4.0;

contract C {
uint[] x; // the data location of x is storage

// the data location of memoryArray is memory
function f(uint[] memoryArray) public {
x = memoryArray; // works, copies the whole array to storage
var y = x; // works, assigns a pointer, data location of y is storage
y[7]; // fine, returns the 8th element
// 局部 storage 变量修改了状态 storage 变量。
y.length = 2; // fine, modifies x through y
// 清除数组的方法
delete x; // fine, clears the array, also modifies y
// 可以把实参赋给状态变量,把局部变量指向状态变量,却不能跳过第一步。状态变量都是“静态分配”的。
// The following does not work; it would need to create a new temporary /
// unnamed array in storage, but storage is "statically" allocated:
// y = memoryArray;
// This does not work either, since it would "reset" the pointer, but there
// is no sensible location it could point to.
// delete y;
g(x); // calls g, handing over a reference to x
h(x); // calls h and creates an independent, temporary copy in memory
}

function g(uint[] storage storageArray) internal {}
function h(uint[] memoryArray) public {}
}

在实验之中,构造函数生成的总是memory类型的变量,拷贝到 storage 变量就可以让它被持久化了。

总结一下:

强制数据位置:
外部函数的参数必须是calldata
状态变量必须是storage
缺省数据位置:
函数参数默认是memory
所有局部变量默认是storage

数组

数组可以有编译时的固定长度,也可以是动态的。storage数组的元素类型可以是任意的(即可以是数组的数组、mapping 的数组和结构体的数组)。memory数组则不能有mapping元素,如果它是个公共可见函数(外部函数?),它的元素必须是个 ABI 类型。

定长数组写作,T[k],变长数组写作T[](也就是说大部分的智能合约的函数参数都是memory动态数组)。

5个unint动态数组的写法与其他语言正好反过来,uint[][15]。访问第三个动态数组的第二个元素,又反而和其它语言一样,x[2][16]

创建内存数组

可以用 new 来创建内存数组memory数组不可以通过.length 来修改数组尺寸,但storage数组可以。

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.16;

contract C {
function f(uint len) public pure {
uint[] memory a = new uint[](7);
bytes memory b = new bytes(len);
// Here we have a.length == 7 and b.length == len
a[6] = 8;
}
}
数组字面量/内联数组
1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.16;

contract C {
function f() public pure {
g([uint(1), 2, 3]);
}
// 这也是个定长数组的例子
function g(uint[3] _data) public pure {
// ...
}
}
成员

length:只有 storage 的动态数组才能动态修改自己的尺寸。
push:在数组尾部添加元素,返回新的长度。

数组的一个综合例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
pragma solidity ^0.4.16;

contract ArrayContract {
uint[2**20] m_aLotOfIntegers;
// Note that the following is not a pair of dynamic arrays but a
// dynamic array of pairs (i.e. of fixed size arrays of length two).
bool[2][] m_pairsOfFlags;
// newPairs is stored in memory - the default for function arguments

function setAllFlagPairs(bool[2][] newPairs) public {
// assignment to a storage array replaces the complete array
m_pairsOfFlags = newPairs;
}

function setFlagPair(uint index, bool flagA, bool flagB) public {
// access to a non-existing index will throw an exception
m_pairsOfFlags[index][0] = flagA;
m_pairsOfFlags[index][17] = flagB;
}

function changeFlagArraySize(uint newSize) public {
// if the new size is smaller, removed array elements will be cleared
m_pairsOfFlags.length = newSize;
}

function clear() public {
// these clear the arrays completely
delete m_pairsOfFlags;
delete m_aLotOfIntegers;
// identical effect here
m_pairsOfFlags.length = 0;
}

bytes m_byteData;

function byteArrays(bytes data) public {
// byte arrays ("bytes") are different as they are stored without padding,
// but can be treated identical to "uint8[]"
m_byteData = data;
m_byteData.length += 7;
m_byteData[3] = byte(8);
delete m_byteData[2];
}

function addFlag(bool[2] flag) public returns (uint) {
return m_pairsOfFlags.push(flag);
}

function createMemoryArray(uint size) public pure returns (bytes) {
// Dynamic memory arrays are created using `new`:
uint[2][] memory arrayOfPairs = new uint[2][](size);
// Create a dynamic byte array:
bytes memory b = new bytes(200);
for (uint i = 0; i < b.length; i++)
b[i] = byte(i);
return b;
}
}
结构体类型

结构体提供了定义新类型的能力。
众筹合约的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
pragma solidity ^0.4.11;

contract CrowdFunding {
// Defines a new type with two fields.
struct Funder {
address addr;
uint amount;
}

struct Campaign {
address beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping (uint => Funder) funders;
}

uint numCampaigns;
mapping (uint => Campaign) campaigns;

function newCampaign(address beneficiary, uint goal) public returns (uint campaignID) {
campaignID = numCampaigns++; // campaignID is return variable
// Creates new struct and saves in storage. We leave out the mapping type.
// 这里构造函数生成的数据类型是 memory,而 campaigns[campaignID]的数据位置是 storage,只有这种方式可以隐式自动转化这两种 data location
campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);
}

function contribute(uint campaignID) public payable {
Campaign storage c = campaigns[campaignID];
// Creates a new temporary memory struct, initialised with the given values
// and copies it over to storage.
// Note that you can also use Funder(msg.sender, msg.value) to initialise.
c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
c.amount += msg.value;
}

function checkGoalReached(uint campaignID) public returns (bool reached) {
Campaign storage c = campaigns[campaignID];
if (c.amount < c.fundingGoal)
return false;
uint amount = c.amount;
c.amount = 0;
c.beneficiary.transfer(amount);
return true;
}
}

struct 本身可以存储数组和映射,也可以被这两者存储。它不能存储它自身类型的变量,struct 的尺寸必须是有限的。

映射

mapping(_KeyType => _ValueType)里的key类型不能是映射,动态长度数组、合约、枚举和结构体。value 可以是任何类型。

映射可以被认为是个被虚拟初始化的 hash 表,所有可能的键都存在而且值被初始化为0值。实际上 key 数据并不存在映射里面,只有它的keccak256散列值被存在里面,用来查值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.0;

contract MappingExample {
mapping(address => uint) public balances;

function update(uint newBalance) public {
balances[msg.sender] = newBalance;
}
}

contract MappingUser {
function f() public returns (uint) {
MappingExample m = new MappingExample();
m.update(100);
return m.balances(this);
}
}

mapping 本身是不可迭代的,但可以看iterable mapping,看看怎么在它之上建立一个可迭代的结构。

牵涉到左值的操作符

delete

delete一个变量,差不多可以说是把一个零值赋给它。除了不能 delete 一个映射以外,所有类型的变量都可以被 delete。delete 动态数组会得到一个长度为0的新动态数组,delete 一个静态数组会得到一个长度等于原长度,所有元素都等于该元素类型零值的数组,delete 一个结构体变量,所有的成员除了映射都会变成0值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.4.0;

contract DeleteExample {
uint data;
uint[] dataArray;

function f() public {
// 注意!这里 x 拷贝了 data,而不是把一个指针指向了 data。
uint x = data;
delete x; // sets x to 0, does not affect data
delete data; // sets data to 0, does not affect x which still holds a copy
uint[] storage y = dataArray;
delete dataArray; // this sets dataArray.length to zero, but as uint[] is a complex object, also
// y is affected which is an alias to the storage object
// On the other hand: "delete y" is not valid, as assignments to local variables
// referencing storage objects can only be made from existing storage objects.
}
}

基本数据类型之间的转换

所有的数据类型都可以转变为uint160,也就可以转变为地址。

显式转换的例子:

1
uint x = uint(y);

类型推导

1
2
uint24 x = 0x123;
var y = x;

var 不能用在函数形参和返回值上。
var 只在第一次赋值时被推导出来,所以以下循环无法终止:

1
for (var i = 0; i < 2000; i++) { ... }

因为第一次赋值,使得 i 被推导为uint8类型了。

单位和全局可用变量

以太币的单位

四级单位weifinneyszabo 或者 ether2 ether == 2000 finney

时间单位

secondsminuteshoursdayweeksyears
小心闰秒( leap seconds)问题。

巧妙使用单位来对比当前时间的例子:

1
2
3
4
5
function f(uint start, uint daysAfter) public {
if (now >= start + daysAfter * 1 days) {
// ...
}
}

特殊的变量和函数

在全局空间里早已存在一些特殊的变量和函数,用以提供区块链的信息:

区块和事务属性
  • block.blockhash(uint blockNumber) returns (bytes32):一个给定区块的散列值 - 只对当前最近的256个区块奏效。
  • block.coinbase (address):当前区块挖掘者的地址。
  • block.difficulty (uint):当前区块的难度。
  • block.gaslimit (uint):当前区块的 gaslimit。
  • block.number (uint):当前区块号
  • block.timestamp (uint):当前区块的时间戳,从 unix epoch 开始的秒数形式。
  • msg.data (bytes):完整的调用数据。
  • msg.gas (uint):剩余 gas。
  • msg.sender (address):当前调用的消息发送者。
  • msg.sig (bytes4):calldata 的最初四个字节(函数标识符)。
  • msg.value (uint):消息附带的价值数。
  • now (uint):时间戳的别名。
  • tx.gasprice (uint):事务的 gasprice。
  • tx.origin (address):事务的发送者 (完整调用链)。

msg 的所有成员,可能在每个外部调用中都发生变化。

错误处理

assert(bool condition):如果条件不为真抛出异常。
require(bool condition):抛出异常如果条件不为真,用来做输入检查(通常配合 modifier 使用)。
revert():退出执行,并且反转状态变化。

数学和密码学函数
  • addmod(uint x, uint y, uint k) returns (uint):
    compute (x + y) % k where the addition is performed with arbitrary precision and does not wrap around at 2**256. Assert that k != 0 starting from version 0.5.0.
  • mulmod(uint x, uint y, uint k) returns (uint):
    compute (x y) % k where the multiplication is performed with arbitrary precision and does not wrap around at 2*256. Assert that k != 0 starting from version 0.5.0.
  • keccak256(…) returns (bytes32):
    compute the Ethereum-SHA-3 (Keccak-256) hash of the (tightly packed) arguments
  • sha256(…) returns (bytes32):
    compute the SHA-256 hash of the (tightly packed) arguments
  • sha3(…) returns (bytes32):
    alias to keccak256
  • ripemd160(…) returns (bytes20):
    compute RIPEMD-160 hash of the (tightly packed) arguments
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):
    recover the address associated with the public key from elliptic curve signature or return zero on error (example usage)

因为“紧密打包”,以下内容是相等的:

1
2
3
4
5
keccak256("ab", "c")
keccak256("abc")
keccak256(0x616263)
keccak256(6382179)
keccak256(97, 98, 99)

表达式和控制结构

输入参数和输出参数

输入参数

声明输入参数和声明变量一样。

输出参数

输出参数的定义,很像 Golang:

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.16;

contract Simple {
function arithmetics(uint _a, uint _b)
public
pure
returns (uint o_sum, uint o_product)
{
o_sum = _a + _b;
o_product = _a * _b;
}
}

控制结构

除了 switch 和 goto,JavaScript 中所有的控制结构 Solidity 都支持。

多返回值

我们已经看到声明的地方有多返回值了,可以像 Golang 一样写多返回值-return (v0, v1, ..., vn)

Function Calls

内部调用
1
2
3
4
5
6
pragma solidity ^0.4.16;

contract C {
function g(uint a) public pure returns (uint ret) { return f(); }
function f() internal pure returns (uint ret) { return g(7) + f(); }
}

内部调用会被 EVM 翻译成字节码,直接 jump 过去。因为这时候还是在内存内调用,内存内状态没有被清理掉,所以是很高效。

外部函数调用

c.g(2);这类调用,本身是在一个 contract 实例上的调用,本身必然要求 message call 的,就不能 jump 了。

调用其他合约的 payable 方法的例子。

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.0;

contract InfoFeed {
function info() public payable returns (uint ret) { return 42; }
}

contract Consumer {
InfoFeed feed;
function setFeed(address addr) public { feed = InfoFeed(addr); }
function callFeed() public { feed.info.value(10).gas(800)(); }
}

只有被 payable 修饰的方法,才可以被 value option 方法调用。InfoFeed(addr)并不是调用一个构造方法(看来构造方法只有在合约初始化的时候才会被调用),而是向这个地址声称能一个代理对象?

与任何其他合约交互都有潜在危险,特别是在该合约的源码事先未知的情况下。当前的合约把控制权拱手交给被调用合约,它就可以做任何事。为了让自己的成not vulnerable to exploit,应该让自己的外部函数调用有防御性,尽量让外部函数调用发生在状态改变之后

命名调用和匿名函数参数

一个简单的字面量调用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;

contract C {
function f(uint key, uint value) public {
// ...
}

function g() public {
// named arguments
f({value: 2, key: 3});
}
}
省略函数参数名
1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.16;

contract C {
// 这里有个被省略了的 uint 参数,不能访问到。
// omitted name for parameter
function func(uint k, uint) public pure returns(uint) {
return k;
}
}

new创建新合约

可以用new来创建合约。要被创建的合约的完整代码,要事先被知道,所以,也就不可能有递归的创建依赖了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.0;

contract D {
uint x;
function D(uint a) public payable {
x = a;
}
}

contract C {
// 创建 C 的时候一并创建 D
D d = new D(4); // will be executed as part of C's constructor

function createD(uint arg) public {
D newD = new D(arg);
}

function createAndEndowD(uint arg, uint amount) public payable {
// Send ether along with the creation
D newD = (new D).value(amount)(arg);
}
}

表达式求值的顺序

除了布尔表达式以外,所有表达式的求值顺序都可能是未定的。在表达式树中,只有一个节点的子节点的孩子节点一定会在它之前执行(后缀遍历),其他的全部都没有保证。

赋值

解构赋值和多返回值

Solidity 内部支持元组类型,即一个元素可能是不同类型,但长度在编译时就已经确定为常数的数组列表。元组才是多返回值的本质(其实在其他语言里还允许对象结构的解构返回)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
pragma solidity ^0.4.16;

contract C {
uint[] data;

function f() public pure returns (uint, bool, uint) {
return (7, true, 2);
}

function g() public {
// Declares and assigns the variables. Specifying the type explicitly is not possible.
var (x, b, y) = f();
// Assigns to a pre-existing variable.
(x, y) = (2, 7);
// Common trick to swap values -- does not work for non-value storage types.
(x, y) = (y, x);
// Components can be left out (also for variable declarations).
// If the tuple ends in an empty component,
// the rest of the values are discarded.
(data.length,) = f(); // Sets the length to 7
// The same can be done on the left side.
// If the tuple begins in an empty component, the beginning values are discarded.
(,data[3]) = f(); // Sets data[3] to 2
// Components can only be left out at the left-hand-side of assignments, with
// one exception:
(x,) = (1,);
// (1,) is the only way to specify a 1-component tuple, because (1) is
// equivalent to 1.
}
}
数组和结构体的复杂点

数组和结构体等非值类型赋值语义稍显复杂。

赋值给一个状态变量总是会产生一个独立拷贝。

从状态变量赋数组和结构体给局部变量是传引用,再赋值还是传引用。

作用域和声明

一个变量(包括在函数内)被声明的时候就会拥有字节零值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.0;

contract C {
function foo() public pure returns (uint) {
// baz is implicitly initialized as 0
uint bar = 5;
if (true) {
bar += baz;
} else {
uint baz = 10;// never executes
}
return bar;// returns 5
}
}

函数内的变量里还是没有块级作用域的。

从 0.5.0 开始就有作用域了

块作用域有块生命周期。
for 相关变量只有 for 块的生命周期。

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.0;
pragma experimental "v0.5.0";
contract C {
function minimalScoping() pure public {
{
uint same2 = 0;
}

{
uint same2 = 0;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;
pragma experimental "v0.5.0";
contract C {
function f() pure public returns (uint) {
uint x = 1;
{
x = 2; // this will assign to the outer variable
uint x;
}
return x; // x has value 2
}
}

错误处理:断言、需求、反转和异常

所有异常都会反转当前的调用及其子调用的状态变化。

在未来throw会被淘汰掉,应该使用revert

有些调用方法是靠返回值来确认异常的,注意检查返回值。

目前不支持 catch 语句。

合约

Solidity 里的合约,类似面向对象语言里面的类。他们包含持久化状态和修改状态的函数。

跨合约调用不能引用老上下文的状态变量,但依然要小心外部函数调用问题。

创建合约

可以用外部事务或者在合约内部创建合约。

可以用web3.js的 API 创建合约,具体参见web3.eth.Contract

只允许一个构造函数,不允许构造函数重载。也可以没有构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
pragma solidity ^0.4.16;

contract OwnedToken {
// TokenCreator is a contract type that is defined below.
// It is fine to reference it as long as it is not used
// to create a new contract.
TokenCreator creator;
address owner;
bytes32 name;

// This is the constructor which registers the
// creator and the assigned name.
function OwnedToken(bytes32 _name) public {
// internal 变量不需要 this 访问,反正这时候合约还不存在呢
// State variables are accessed via their name
// and not via e.g. this.owner. This also applies
// to functions and especially in the constructors,
// you can only call them like that ("internally"),
// because the contract itself does not exist yet.
owner = msg.sender;
// We do an explicit type conversion from `address`
// to `TokenCreator` and assume that the type of
// the calling contract is TokenCreator, there is
// no real way to check that.
creator = TokenCreator(msg.sender);
name = _name;
}

function changeName(bytes32 newName) public {
// Only the creator can alter the name --
// the comparison is possible since contracts
// are implicitly convertible to addresses.
if (msg.sender == address(creator))
name = newName;
}

function transfer(address newOwner) public {
// Only the current owner can transfer the token.
if (msg.sender != owner) return;
// We also want to ask the creator if the transfer
// is fine. Note that this calls a function of the
// contract defined below. If the call fails (e.g.
// due to out-of-gas), the execution here stops
// immediately.
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}

contract TokenCreator {
function createToken(bytes32 name)
public
returns (OwnedToken tokenAddress)
{
// Create a new Token contract and return its address.
// From the JavaScript side, the return type is simply
// `address`, as this is the closest type available in
// the ABI.
return new OwnedToken(name);
}

function changeName(OwnedToken tokenAddress, bytes32 name) public {
// Again, the external type of `tokenAddress` is
// simply `address`.
tokenAddress.changeName(name);
}

function isTokenTransferOK(address currentOwner, address newOwner)
public
view
returns (bool ok)
{
// 这段可以说是胡诌的
// Check some arbitrary condition.
address tokenAddress = msg.sender;
return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff);
}
}

可见性和 Getter

内部调用只产生 JUMP,而外部调用产生 EVM call。

有四种可见性:externalpublicinternal或者 private。这四个可见性,恰好是两组反义词。

状态变量默认可见性是internal而不可能是external

函数默认可见性是 public!

external:外部函数是合约接口的一部分。意味着他们应该被从外部调用,必然产生 EVM call。一个externalf是不能f()的,只能this.f()

public:公共函数也是合约接口的一部分。可以外部调用也可以内部调用。最简单而完美的接口设计。公共变量会自动产生一个 getter 函数。

internal:内部函数和变量只能合约内部访问,不用 this。类似 private。

private:内部函数和变量不能被继承,只能内部访问。

注意,合约的所有内容在区块链上都是公开可见的,这些修饰符只能阻止不当修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// This will not compile
pragma solidity ^0.4.0;

contract C {
uint private data;

function f(uint a) private returns(uint b) { return a + 1; }
function setData(uint a) public { data = a; }
function getData() public returns(uint) { return data; }
function compute(uint a, uint b) internal returns (uint) { return a+b; }
}

contract D {
function readData() public {
C c = new C();
uint local = c.f(7); // error: member `f` is not visible
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // error: member `compute` is not visible
}
}

// 通过继承,实现了代码复用
contract E is C {
function g() public {
C c = new C();
uint val = compute(3, 5); // access to internal member (from derived to parent contract)
}
}
Getter

getter 是自动生成的,一个调用的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.0;

contract C {
uint public data = 42;
}

contract Caller {
C c = new C();
function f() public {
// 换言之,用函数来访问公共变量,不要用点操作符。
uint local = c.data();
}
}

内外访问与内部访问:

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;

contract C {
uint public data;
function x() public {
data = 3; // internal access
uint val = this.data(); // external access
}
}

一个更复杂的例子:

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.0;

contract Complex {
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
}
mapping (uint => mapping(bool => Data[])) public data;
}

生成的函数的形式很费解:

1
2
3
4
function data(uint arg1, bool arg2, uint arg3) public returns (uint a, bytes3 b) {
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
}

函数修饰符

函数修饰符是用来改变函数行为的,比如检验条件。函数修饰符可以被集成也可以被覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
pragma solidity ^0.4.11;

contract owned {
function owned() public { owner = msg.sender; }
address owner;

// This contract only defines a modifier but does not use
// it: it will be used in derived contracts.
// The function body is inserted where the special symbol
// `_;` in the definition of a modifier appears.
// This means that if the owner calls this function, the
// function is executed and otherwise, an exception is
// thrown.
modifier onlyOwner {
require(msg.sender == owner);
_;
}
}

contract mortal is owned {
// This contract inherits the `onlyOwner` modifier from
// `owned` and applies it to the `close` function, which
// causes that calls to `close` only have an effect if
// they are made by the stored owner.
function close() public onlyOwner {
selfdestruct(owner);
}
}

contract priced {
// Modifiers can receive arguments:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}

contract Register is priced, owned {
mapping (address => bool) registeredAddresses;
uint price;

function Register(uint initialPrice) public { price = initialPrice; }

// 如果没有这个关键字,合约会自动拒绝被发送过来的以太币
// It is important to also provide the
// `payable` keyword here, otherwise the function will
// automatically reject all Ether sent to it.
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}

function changePrice(uint _price) public onlyOwner {
price = _price;
}
}

// 巧妙地加锁,类似 synchronized。
contract Mutex {
bool locked;
modifier noReentrancy() {
require(!locked);
locked = true;
// 让步修饰符,类似 Ruby 中的 yield。
_;
locked = false;
}

/// This function is protected by a mutex, which means that
/// reentrant calls from within `msg.sender.call` cannot call `f` again.
/// The `return 7` statement assigns 7 to the return value but still
/// executes the statement `locked = false` in the modifier.
function f() public noReentrancy returns (uint) {
require(msg.sender.call());
return 7;
}
}

状态常量

状态变量可以被声明为constant。它们必须被编译时已知常量值的表达式赋值,表达式不能接触 storage (e.g. nowthis.balance 或者 block.number),不能由执行数据决定(msg.value或者gasleft()),或者外部合约调用。允许使用内部计算函数keccak256sha256ripemd160ecrecoveraddmodmulmod

1
2
3
4
5
6
7
pragma solidity ^0.4.0;

contract C {
uint constant x = 32**22 + 8;
string constant text = "abc";
bytes32 constant myHash = keccak256("abc");
}

函数

view

不修改状态的函数可以被声明为view

以下情况不能用view

  1. 写状态变量
  2. 发射事件
  3. 创建其他合约
  4. 使用自毁
  5. 用调用发送以太币(也就是说 call 也不安全)
  6. 调用其他非view或者pure方法。
  7. 使用低级调用方法。
  8. 使用某些特定内联汇编操作码。
1
2
3
4
5
6
7
pragma solidity ^0.4.16;

contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + now;
}
}

getter 都是view

view 是尽量做到最大限度的编程安全,有时候又不能保证。所以是个未完成品。

pure 函数

不读和写 state 的函数叫 pure 函数。view 是不写,pure 更彻底。pure 可能产生副作用,不是函数式编程里的纯函数。

pure 函数首先必须满足 view 函数的要求,然后不能有以下行为:

  1. 读状态变量
  2. 访问任何 balance。
  3. 访问 block,tx 和 msg 的任何成员。
  4. 调用非pure的其他函数。
  5. 使用某些特定内联汇编操作码。

pure 也是未完成品,编译器不能保证函数的不读不写。

降级函数

一个合约只能由一个无名函数。这个函数不能有参数列表,也不能有返回值。合约在被调用,但没有合适的函数匹配得上,或者没有任何调用数据的时候,这个函数就被派上用场了。

这个函数在合约接收到纯以太币(没有数据)的时候,这个函数也会被执行。为了接收以太币,这个函数必须被声明为 payable。没有这个函数,普通转账事务无法往这个合约里转账。

这个函数最少需要 2300个 gas(恰好是普通转账的十分之一)。

即使这个函数没有任何参数,它也可以用msg.data来获取调用载荷。换言之,调用这种函数,应该发送一些没有其他函数可以匹配得上的实参。

早版本的 Solidity 里面降级函数都调用失败的话,是不退币的,现在的版本退币了。

即使没有降级函数,自毁和挖矿的目标依然可以设置为这个合约账户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pragma solidity ^0.4.0;

contract Test {
// This function is called for all messages sent to
// this contract (there is no other function).
// Sending Ether to this contract will cause an exception,
// because the fallback function does not have the `payable`
// modifier.
function() public { x = 1; }
uint x;
}


// This contract keeps all Ether sent to it with no way
// to get it back.
contract Sink {
function() public payable { }
}

contract Caller {
function callTest(Test test) public {
test.call(0xabcdef01); // hash does not exist
// results in test.x becoming == 1.

// The following will not compile, but even
// if someone sends ether to that contract,
// the transaction will fail and reject the
// Ether.
//test.send(2 ether);
}
}
函数重载

大部分情况下同其他重载一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// This will not compile
pragma solidity ^0.4.16;

contract A {
function f(B _in) public pure returns (B out) {
out = _in;
}

function f(address _in) public pure returns (address out) {
out = _in;
}
}

contract B {
}

这个函数之所以编译错误,是因为编译后的 ABI 类型都是 address,等于没有重载。

事件

事件允许方便地使用 EVM 的日志基础设施。

日志可以被在合约内部被继承。日志与事务、合约关联,与区块同在。

日志的 SPV 证明是可以做到的。

最多三个参数可以被标记为indexed来索引。其实索引的查找,可能是通过散列存储和查找的方式来实现的。本质上就是把这个参数存成 topic 类型的数据,可以被类似流处理的机制监听起来。

事件的正统 watch 用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.4.0;

contract ClientReceipt {
event Deposit(
address indexed _from,
bytes32 indexed _id,
uint _value
);

function deposit(bytes32 _id) public payable {
// Events are emitted using `emit`, followed by
// the name of the event and the arguments
// (if any) in parentheses. Any such invocation
// (even deeply nested) can be detected from
// the JavaScript API by filtering for `Deposit`.
emit Deposit(msg.sender, _id, msg.value);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var abi = /* abi as generated by the compiler */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* address */);

var event = clientReceipt.Deposit();

// watch for changes
event.watch(function(error, result){
// result will contain various information
// including the argumets given to the `Deposit`
// call.
if (!error)
console.log(result);
});

// Or pass a callback to start watching immediately
var event = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});

低级日志设施
1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.10;

contract C {
function f() public payable {
bytes32 _id = 0x420042;
log3(
bytes32(msg.value),
bytes32(0x50cb9fe53daa9737b786ab3646f04d0150dc50ef4e75f59509d83667ad5adb20),
bytes32(msg.sender),
_id
);
}
}

logn 一共取 n+1个参数。第一个参数放在 data 区,其他参数放在 topic 区。

其他理解事件的资源

继承

Solidity 支持某种程度上的多继承,它需要拷贝包括多态在内的代码。当一个合约多继承的时候,在链上只创建一份合约,其他

所有的函数调用都是虚调用。

继承系统非常像 Python 的元类继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
pragma solidity ^0.4.16;

contract owned {
function owned() { owner = msg.sender; }
address owner;
}

// 只有 private 不会被继承下来
// Use `is` to derive from another contract. Derived
// contracts can access all non-private members including
// internal functions and state variables. These cannot be
// accessed externally via `this`, though.
contract mortal is owned {
function kill() {
if (msg.sender == owner) selfdestruct(owner);
}
}

// 没有函数体的函数。其合约是抽象的。完全没有函数体的合约只能当接口用。
// These abstract contracts are only provided to make the
// interface known to the compiler. Note the function
// without body. If a contract does not implement all
// functions it can only be used as an interface.
contract Config {
function lookup(uint id) public returns (address adr);
}

contract NameReg {
function register(bytes32 name) public;
function unregister() public;
}

// 多继承类似 C++ 的虚继承,不会有多份重复成员。
// Multiple inheritance is possible. Note that `owned` is
// also a base class of `mortal`, yet there is only a single
// instance of `owned` (as for virtual inheritance in C++).
contract named is owned, mortal {
function named(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}

// Functions can be overridden by another function with the same name and
// the same number/types of inputs. If the overriding function has different
// types of output parameters, that causes an error.
// Both local and message-based function calls take these overrides
// into account.
function kill() public {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// It is still possible to call a specific
// overridden function.
mortal.kill();
}
}
}

// 这样调用显式构造函数就讨厌了
// If a constructor takes an argument, it needs to be
// provided in the header (or modifier-invocation-style at
// the constructor of the derived contract (see below)).
contract PriceFeed is owned, mortal, named("GoldFeed") {
function updateInfo(uint newInfo) public {
if (msg.sender == owner) info = newInfo;
}

function get() public view returns(uint r) { return info; }

uint info;
}

一个菱形继承的讨厌问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.0;

contract owned {
function owned() public { owner = msg.sender; }
address owner;
}

contract mortal is owned {
function kill() public {
if (msg.sender == owner) selfdestruct(owner);
}
}

contract Base1 is mortal {
function kill() public { /* do cleanup 1 */ super.kill(); }
}


contract Base2 is mortal {
function kill() public { /* do cleanup 2 */ super.kill(); }
}

contract Final is Base1, Base2 {
}

继承顺序大概是Final,Base2,Base1,mortal,owned。

调用顺序很复杂,需要用的时候还是看教程原文吧。

构造器

构造器可以是public也可以是internal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.11;

contract A {
uint public a;

// A 因为这个构造器变成了抽象类
function A(uint _a) internal {
a = _a;
}
}

// 显式实例化
contract B is A(1) {
function B() public {}
}
基类构造器参数
1
2
3
4
5
6
7
8
9
10
11
12
13
pragma solidity ^0.4.0;

contract Base {
uint x;
function Base(uint _x) public { x = _x; }
}

// 用两种方法调用基类构造器
contract Derived is Base(7) {
// 第二种方法赢了
function Derived(uint _y) Base(_y * _y) public {
}
}
多重继承与线性化

Solidity 学习 Python 的C3线性化来解决菱形问题

1
2
3
4
5
6
7
// This will not compile

pragma solidity ^0.4.0;

contract X {}
contract A is X {}
contract C is A, X {}
抽象合约

两种情况下会产生抽象合约:

  • 有函数没有函数体
  • 构造函数是internal的。
1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;

contract Feline {
function utterance() public returns (bytes32);
}

contract Cat is Feline {
function utterance() public returns (bytes32) { return "miaow"; }
}

对比

1
2
3
4
// 无函数体函数
function foo(address) external returns (address);
// 函数变量
function(address) external returns (address) foo;

接口

接口就是完全没有任何函数体的合约。和 cpp 里抽象类到接口的顺序是一致的。

而且还有以下很熟悉的限制:

  • 不能继承其他合约和接口
  • 不能定义构造器
  • 不能定义变量
  • 不能定义结构体
  • 不能定义枚举

接口就是 ABI 的等价形式,两者之间应该可以无损互转。

接口有自己的关键字。

1
2
3
4
5
pragma solidity ^0.4.11;

interface Token {
function transfer(address recipient, uint amount) public;
}

使用接口和继承其他合约一样,也就是都是 is 关键字,没有其他用法。

库类似于合约。他们的目的是只在特定的地址上部署一次,然后通过DELEGATECALL调用。这意味着,库函数会在当前合约的上下文里被调用,反而不像外部调用(直接调用其他合约地址的实例方法一样),产生 EVM call。函数里的 this 都会绑定到当前合约,类似 bind。

当然,代理调用是底层实现的,在使用上是看不出来的。库的internal函数,对于调用它的合约也是可见的,这些代码就好像是一个被 mixin 进合约里的基类合约一样。调用内部函数用的是JUMP而不是DELEGATECALL

在这里,库是一个 util 式的用法。换言之,库里必须都是是view或者pure函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
pragma solidity ^0.4.16;

library Set {
// We define a new struct datatype that will be used to
// hold its data in the calling contract.
struct Data { mapping(uint => bool) flags; }

// Note that the first parameter is of type "storage
// reference" and thus only its storage address and not
// its contents is passed as part of the call. This is a
// special feature of library functions. It is idiomatic
// to call the first parameter `self`, if the function can
// be seen as a method of that object.
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}

function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}

function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}

contract C {
Set.Data knownValues;

function register(uint value) public {
// The library functions can be called without a
// specific instance of the library, since the
// "instance" will be the current contract.
require(Set.insert(knownValues, value));
}
// In this contract, we can also directly access knownValues.flags, if we want.
}

库也有限制:

  • 不能有状态变量
  • 不能继承其他也不能被继承
  • 不能接收以太币

Using For

using A for B; 表明,把一个库的函数添加到一个指定类型上。这导致了这些库函数多了一个(隐式的)消息接受者对象作为第一个参数,这类似 Python 里的 self。

using A for *;表明 A 的库函数被附着给任意类型。这可能也是为什么库不能有自己的 state 的原因吧。

没有用到 self 的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
pragma solidity ^0.4.16;

library BigInt {

// 这个结构体本来是没有成员函数的。
struct bigint {
uint[] limbs;
}

// 全部都是 pure 函数
function fromUint(uint x) internal pure returns (bigint r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}

function add(bigint _a, bigint _b) internal pure returns (bigint r) {
r.limbs = new uint[](max(_a.limbs.length, _b.limbs.length));
uint carry = 0;
for (uint i = 0; i < r.limbs.length; ++i) {
uint a = limb(_a, i);
uint b = limb(_b, i);
r.limbs[i] = a + b + carry;
if (a + b < a || (a + b == uint(-1) && carry > 0))
carry = 1;
else
carry = 0;
}
if (carry > 0) {
// too bad, we have to add a limb
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}

function limb(bigint _a, uint _limb) internal pure returns (uint) {
return _limb < _a.limbs.length ? _a.limbs[_limb] : 0;
}

function max(uint a, uint b) private pure returns (uint) {
return a > b ? a : b;
}
}

contract C {
using BigInt for BigInt.bigint;

function f() public pure {
// 这里的三个变量类型都是 BigInt.bigint,但增加了库函数
var x = BigInt.fromUint(7);
var y = BigInt.fromUint(uint(-1));
var z = x.add(y);
}
}

self 可以不是必须的,也可以有,另一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
pragma solidity ^0.4.16;

// This is the same code as before, just without comments
library Set {
struct Data { mapping(uint => bool) flags; }

function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}

function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}

function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}

contract C {
using Set for Set.Data; // this is the crucial change
// 用名字空间来访问内部类型
Set.Data knownValues;

function register(uint value) public {
// Here, all variables of type Set.Data have
// corresponding member functions.
// The following function call is identical to
// `Set.insert(knownValues, value)`
// 从第二个参数传起
require(knownValues.insert(value));
}
}

对基础类型的猴子补丁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
pragma solidity ^0.4.16;

library Search {
function indexOf(uint[] storage self, uint value)
public
view
returns (uint)
{
for (uint i = 0; i < self.length; i++)
if (self[i] == value) return i;
return uint(-1);
}
}

contract C {
using Search for uint[];
uint[] data;

function append(uint value) public {
data.push(value);
}

function replace(uint _old, uint _new) public {
// This performs the library function call
uint index = data.indexOf(_old);
if (index == uint(-1))
data.push(_new);
else
data[index] = _new;
}
}

Solidity 汇编

Solidity 支持手写汇编模式。

杂项

有用的小技巧

  • 使用delete删除所有的数组元素。
  • 调用内部 send 的方法是address(contractVariable).send(amount)
  • 用字面量的方法初始化结构体(而不是合约):x = MyStruct({a: 1, b: 2});

安全问题

可重入

所有的 send 本质上都可以包含代码执行(因为降级函数的存在),所有以下两个合约都是有漏洞的:

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;

// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract Fund {
/// Mapping of ether shares of the contract.
mapping(address => uint) shares;
/// Withdraw your share.
function withdraw() public {
if (msg.sender.send(shares[msg.sender]))
shares[msg.sender] = 0;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;

// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract Fund {
/// Mapping of ether shares of the contract.
mapping(address => uint) shares;
/// Withdraw your share.
function withdraw() public {
if (msg.sender.call.value(shares[msg.sender])())
shares[msg.sender] = 0;
}
}

被调用方如果在自己的降级函数里面再调用这个合约的提取函数,那么它可以无限提取钱,直到 out-of-gas 异常发生为止。

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.11;

contract Fund {
/// Mapping of ether shares of the contract.
mapping(address => uint) shares;
/// Withdraw your share.
function withdraw() public {
var share = shares[msg.sender];
shares[msg.sender] = 0;
msg.sender.transfer(share);
}
}

只有这个先扣款的钱包才能防别人反向递归调用自己。

转以太币相关问题

一个地址和账户没有办法抗拒别人向它转账,除非转账失败。不用消息调用也可以移动以太币的方法有两个:挖矿和自毁。

addr.call.value(x)()等价于addr.transfer(x),但它把剩余的 gas 全都提供给接受者,让它执行更贵的操作了。

一定要记得检查send的返回值。

tx.origin

一个类似 CSRF 的相关问题:

一个检查 tx.origin 的钱包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.11;

// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract TxUserWallet {
address owner;

function TxUserWallet() public {
owner = msg.sender;
}

function transferTo(address dest, uint amount) public {
require(tx.origin == owner);
dest.transfer(amount);
}
}

一个攻击者的钱包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.11;

interface TxUserWallet {
function transferTo(address dest, uint amount) public;
}

contract TxAttackWallet {
address owner;

function TxAttackWallet() public {
owner = msg.sender;
}

function() public {
TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance);
}
}

只要被攻击者往攻击者钱包转账,攻击者钱包就会发一个新的事务,并借用 tx.origin 来盗取被攻击者的全部余额。每个地址的全部余额都是公开可查的。

通用模式

从合约中取钱

比谁更有钱的合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pragma solidity ^0.4.11;

contract WithdrawalContract {
address public richest;
uint public mostSent;

mapping (address => uint) pendingWithdrawals;

function WithdrawalContract() public payable {
richest = msg.sender;
mostSent = msg.value;
}

function becomeRichest() public payable returns (bool) {
if (msg.value > mostSent) {
pendingWithdrawals[richest] += msg.value;
richest = msg.sender;
mostSent = msg.value;
return true;
} else {
return false;
}
}

function withdraw() public {
uint amount = pendingWithdrawals[msg.sender];
// Remember to zero the pending refund before
// sending to prevent re-entrancy attacks
pendingWithdrawals[msg.sender] = 0;
msg.sender.transfer(amount);
}
}

用 modifier 来设定可访问性的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
pragma solidity ^0.4.11;

contract AccessRestriction {
// These will be assigned at the construction
// phase, where `msg.sender` is the account
// creating this contract.
address public owner = msg.sender;
uint public creationTime = now;

// Modifiers can be used to change
// the body of a function.
// If this modifier is used, it will
// prepend a check that only passes
// if the function is called from
// a certain address.
modifier onlyBy(address _account)
{
require(msg.sender == _account);
// Do not forget the "_;"! It will
// be replaced by the actual function
// body when the modifier is used.
_;
}

/// Make `_newOwner` the new owner of this
/// contract.
function changeOwner(address _newOwner)
public
onlyBy(owner)
{
owner = _newOwner;
}

modifier onlyAfter(uint _time) {
require(now >= _time);
_;
}

/// Erase ownership information.
/// May only be called 6 weeks after
/// the contract has been created.
function disown()
public
onlyBy(owner)
onlyAfter(creationTime + 6 weeks)
{
delete owner;
}

// This modifier requires a certain
// fee being associated with a function call.
// If the caller sent too much, he or she is
// refunded, but only after the function body.
// This was dangerous before Solidity version 0.4.0,
// where it was possible to skip the part after `_;`.
modifier costs(uint _amount) {
require(msg.value >= _amount);
_;
if (msg.value > _amount)
msg.sender.send(msg.value - _amount);
}

function forceOwnerChange(address _newOwner)
public
costs(200 ether)
{
owner = _newOwner;
// just some example condition
if (uint(owner) & 0 == 1)
// This did not refund for Solidity
// before version 0.4.0.
return;
// refund overpaid fees
}
}

状态机

这个例子有两个有意思的地方:

  • modifier 可以叠加
  • 可以由时间自动驱动状态机执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
pragma solidity ^0.4.11;

contract StateMachine {
enum Stages {
AcceptingBlindedBids,
RevealBids,
AnotherStage,
AreWeDoneYet,
Finished
}

// This is the current stage.
Stages public stage = Stages.AcceptingBlindedBids;

uint public creationTime = now;

modifier atStage(Stages _stage) {
require(stage == _stage);
_;
}

function nextStage() internal {
stage = Stages(uint(stage) + 1);
}

// 必须放在最开始
// Perform timed transitions. Be sure to mention
// this modifier first, otherwise the guards
// will not take the new stage into account.
modifier timedTransitions() {
if (stage == Stages.AcceptingBlindedBids &&
now >= creationTime + 10 days)
nextStage();
if (stage == Stages.RevealBids &&
now >= creationTime + 12 days)
nextStage();
// The other stages transition by transaction
_;
}

// Order of the modifiers matters here!
function bid()
public
payable
timedTransitions
atStage(Stages.AcceptingBlindedBids)
{
// We will not implement that here
}

function reveal()
public
timedTransitions
atStage(Stages.RevealBids)
{
}

// This modifier goes to the next stage
// after the function is done.
modifier transitionNext()
{
_;
nextStage();
}

function g()
public
timedTransitions
atStage(Stages.AnotherStage)
transitionNext
{
}

function h()
public
timedTransitions
atStage(Stages.AreWeDoneYet)
transitionNext
{
}

function i()
public
timedTransitions
atStage(Stages.Finished)
{
}
}