0%

Solidity学习笔记

入门说明

在把智能协议传上以太坊之后,它就变得不可更改, 这种永固性意味着你的代码永远不能被调整或更新。你编译的程序会一直,永久的,不可更改的,存在以太坊上。

非常重要的是,部署在以太坊上的 DApp,并不能保证它真正做到去中心,我们需要阅读并理解它的源代码,才能防止其中没有被部署者恶意植入后门;作为开发人员,如何做到既要给自己留下修复 bug 的余地,又要尽量地放权给使用者,以便让他们放心你,从而愿意把数据放在你的 DApp 中,这确实需要个微妙的平衡。

文件结构

1
pragma solidity ^0.4.0
  1. 版本要高于0.4才可以编译
  2. 表示高于0.5的版本则不可编译,第三位的版本号但可以变,留出来用做bug可以修复(如0.4.1的编译器有bug,可在0.4.2修复,现有合约不用改代码)。

引用源文件

全局引入:

1
import “filename”;

自定义命名空间引入:

1
import * as symbolName from “filename”

分别定义引入:

1
import  {symbol1 as alias, symbol2} from “filename”

非es6兼容的简写语法:

1
2
3
import “filename” as symbolName
// 等同于上述
import * as symbolName from “filename”

类型

值类型:

  • 布尔(Booleans)
  • 整型(Integer)
  • 地址(Address)
  • 定长字节数组(fixed byte arrays)
  • 有理数和整型(Rational and Integer Literals,String literals)
  • 枚举类型(Enums)
  • 函数(Function Types)

引用类型

  • 不定长字节数组(bytes)
  • 字符串(string)
  • 数组(Array)
  • 结构体(Struts)

数据位置

复杂类型,如数组(arrays)和数据结构(struct)在Solidity中有一个额外的属性,数据的存储位置。可选为memory和storage。

memory存储位置同我们普通程序的内存一致。即分配,即使用,越过作用域即不可被访问,等待被回收。而在区块链上,由于底层实现了图灵完备,故而会有非常多的状态需要永久记录下来,那么我们就要使用storage类型了,一旦使用这个类型,数据将永远存在。

基于程序的上下文,大多数时候这样的选择是默认的,我们可以通过指定关键字storage和memory修改它。默认的函数参数,包括返回的参数,他们是memory。默认的局部变量是storage的。默认的状态变量(合约声明的公有变量)是storage。

另外还有第三个存储位置calldata。它存储的是函数参数,是只读的,不会永久存储的一个数据位置。外部函数的参数(不包括返回参数)被强制指定为calldata。效果与memory差不多。

  • storage转换为storage只是修改了它的指针。
  • 将一个memory类型的变量赋值给一个状态变量时,实际是将内存变量拷贝到存储中。
  • memory赋值给局部变量:局部变量虽然是一个storage的,但它仅仅是一个storage类型的指针,所以直接赋值会报错,不能将memory赋值给局部变量.
  • 将storage转为memory,实际是将数据从storage拷贝到memory中。
  • 将一个memory的引用类型赋值给另一个memory的引用,不会创建拷贝(即:memory之间是引用传递)。
  • 对于值类型,总是会进行拷贝。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.4.0;

contract DataLocation{
uint valueType;
mapping(uint => uint) public refrenceType;

function changeMemory(){
var tmp = valueType;
tmp = 100;
}

function changeStorage(){
var tmp = refrenceType;
tmp[1] = 100;
}

function getAll() returns (uint, uint){
return (valueType, refrenceType[1]);
}
}

强制的数据位置:

  • 外部函数(External function)的参数(不包括返回参数)强制为:calldata
  • 状态变量(State variables)强制为: storage

默认数据位置:

  • 函数参数(包括返回参数):memory
  • 所有其它的局部变量:storage

布尔

bool可能的取值为常量值true和false

整型

int/uint:

  • 变长的有符号或无符号整型。变量支持的步长以8递增,支持从uint8到uint256,以及int8到int256。uint和int默认代表的是uint256和int256

字面量:

  • 整数字面量,由包含0-9的数字序列组成,默认被解释成十进制。在Solidity中不支持八进制,前导0会被默认忽略,如0100,会被认为是100。
  • 小数由.组成,在他的左边或右边至少要包含一个数字。如1.,.1,1.3均是有效的小数。

字面量本身支持任意精度,也就是可以不会运算溢出,或除法截断。但当它被转换成对应的非字面量类型,如整数或小数。或者将他们与非字面量进行运算,则不能保证精度了。总之就是,字面量怎么都计算都行,但一旦转为对应的变量后,再计算就不保证精度了。

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

contract IntegerLiteral{
function integerTest() returns (uint, uint){
//超出运算字长了
var i = (2**800 + 1) - 2**800;
var j = 1/3*3;
//小数运算
var k = 0.5*8;
return (i, j);
}
}

十六进制字面量,以关键字hex打头,后面紧跟用单或双引号包裹的字符串。如hex”001122ff”。在内部会被表示为二进制流。由于一个字节是8位,所以一个hex是由两个[0-9a-z]字符组成的,不是成双的字符串是会报错的。十六进制的字面量与字符串可以进行同样的类似操作:

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

contract HexLiteralBytes{
function test() returns (bytes4, bytes1, bytes1, bytes1, bytes1){
bytes4 a = hex"001122FF";

return (a, a[0], a[1], a[2], a[3]);
}
}

地址

address:以太坊地址的长度,大小20个字节,160位,所以可以用一个uint160编码。地址是所有合约的基础,所有的合约都会继承地址对象(注意:从0.5.0开始,合约不再继承自地址类型,但仍然可以显式转换为地址),也可以随时将一个地址串,得到对应的代码进行调用。当然地址代表一个普通帐户时,就没有这么多丰富的功能了。

地址类型的成员:

  • 属性:balance
  • 函数:send(),call(),delegatecall(),callcode()。

十六进制的字符串,凡是能通过地址合法性检查(address checksum test)2,就会被认为是地址,如0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF。需要注意的是39到41位长的没有通过地址合法性检查的,会提示一个警告,但会被视为普通的有理数字面量。

如果只是想得到当前合约的余额,可以这样写:

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

contract addressTest{
function getBalance() returns (uint){
return this.balance;
}
}

transfer()

transfer()用来发送以太币(以wei为单位)

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

如果x是合约地址,合约的回退函数(fallback 函数)会随transfer调用一起执行(这个是EVM特性),如果因gas耗光或其他原因失败,转移交易会还原并且合约会抛异常停止。

send()

用来向某个地址发送货币(货币单位是wei)。

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

contract PayTest {
//得到当前合约的余额
function getBalance() returns (uint) {
return this.balance;//0
}

//向当前合约存款
function deposit() payable returns(address addr, uint amount, bool success){
//msg.sender 全局变量,调用合约的发起方
//msg.value 全局变量,调用合约的发起方转发的货币量,以wei为单位。
//send() 执行的结果
return (msg.sender, msg.value, this.send(msg.value));
}
}

这个合约实现的是充值。this.send(msg.value)意指向合约自身发送msg.value量的以太币。msg.value是合约调用方附带的以太币。

send()方法执行时有一些风险:

  • send与transfer对应,但更底层。如果执行失败,transfer不会因异常停止,而send会返回false。
  • 调用递归深度不能超1024。
  • 如果gas不够,执行会失败。
  • 所以使用这个方法要检查成功与否。或为保险起见,货币操作时要使用一些最佳实践。如果执行失败,将会回撤所有交易,所以务必留意返回结果。

为了同一些不支持ABI协议的进行直接交互(一般的web3.js,soldity都是支持的)。可以使用call()函数,用来向另一个合约发送原始数据。参数支持任何类型任意数量。每个参数会按规则(规则是按ABI)打包成32字节并一一拼接到一起。call()方法支持ABI协议定义的函数选择器。如果第一个参数恰好4个字节,在这种情况下,会被认为根据ABI协议定义的函数器指定的函数签名。所以如果你只是想发送消息体,需要避免第一个参数是4个字节。call方法返回一个bool值,以表明执行成功还是失败。正常结束返回true,异常终止返回false。我们无法解析返回结果,因为这样我们得事前知道返回的数据的编码和数据大小(这里的潜在假设是不知道对方使用的协议格式,所以也不会知道返回的结果如何解析,有点祼协议测试的感觉)。

同样也可以使用delegatecall(),它与call方法的区别在于,仅仅是代码会执行,而其它方面,如(存储,余额等)都是用的当前的合约的数据。delegatecall()方法的目的是用来执行另一个合约中的工具库。所以开发者需要保证两个合约中的存储变量能兼容,来保证delegatecall()能顺利执行。

上面的这三个方法call(),delegatecall(),callcode()都是底层的消息传递调用,最好仅在万不得已才进行使用,因为他们破坏了Solidity的类型安全。上述的函数都是底层的函数,使用时要异常小心。当调用一个未知的,可能是恶意的合约时,当你把控制权交给它,它可能回调回你的合约,所以要准备好在调用返回时,应对你的状态变量可能被恶意篡改的情况。

定长字节数组

bytes1, … ,bytes32,允许值以步长1递增。byte默认表示byte1。成员变量length。

枚举类型(enum)

枚举类型是在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
pragma solidity ^0.4.0;

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

function setGoStraight() {
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() returns (ActionChoices) {
return choice;
}

function getDefaultChoice() returns (uint) {
return uint(defaultChoice);
}
}

函数

函数的分类:

  • 内部函数(internal):因为不能在当前合约的上下文环境以外的地方执行,内部函数只能在当前合约内被使用。如在当前的代码块内,包括内部库函数,和继承的函数中。
  • 外部函数(External):外部函数由地址和函数方法签名两部分组成。可作为外部函数调用的参数,或者由外部函数调用返回。

函数的定义:

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

若不写类型,默认的函数类型是internal的。如果函数没有返回结果,则必须省略returns关键字。

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

contract Test{
//默认是internal类型的
function noParameter() returns (uint){}

//无返回结果
function noReturn1(uint x) {}

//如果无返回结果,必须省略`returns`关键字
//function noReturn2(uint x) returns {}
}

函数的internal与external:调用一个函数f()时,我们可以直接调用f(),或者使用this.f()。但两者有一个区别。前者是通过internal的方式在调用,而后者是通过external的方式在调用。请注意,这里关于this的使用与大多数语言相背。下面通过一个例子来了解他们的不同:

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
pragma solidity ^0.4.5;

contract FuntionTest{
function internalFunc() internal{}

function externalFunc() external{}

function callFunc(){
//直接使用内部的方式调用
internalFunc();

//不能在内部调用一个外部函数,会报编译错误。
//Error: Undeclared identifier.
//externalFunc();

//不能通过`external`的方式调用一个`internal`
//Member "internalFunc" not found or not visible after argument-dependent lookup in contract FuntionTest
//this.internalFunc();

//使用`this`以`external`的方式调用一个外部函数
this.externalFunc();
}
}

contract FunctionTest1{
function externalCall(FuntionTest ft){
//调用另一个合约的外部函数
ft.externalFunc();

//不能调用另一个合约的内部函数
//Error: Member "internalFunc" not found or not visible after argument-dependent lookup in contract FuntionTest
//ft.internalFunc();
}
}

不定长字节数组

  • bytes:动态长度的字节数组。

字符串

字符串(string)字面量是指由单引号,或双引号引起来的字符串。字符串并不像C语言,包含结束符,“foo”这个字符串大小仅为三个字节。字符串的长度类型可以是变长的,特殊之处在于,可以隐式的转换为byte1,…byte32。

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

contract StringConvert{
function test() returns (bytes3){
bytes3 a = "123";

//bytes3 b = "1234";
//Error: Type literal_string "1234" is not implicitly convertible to expected type bytes3.

return a;
}
}
// 上述的字符串字面量,会隐式转换为bytes3。

数组

创建一个数组

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

contract Test {
uint[5] arr = [0,1,2,3,4];//创建一个定长的数组
uint[] storageArr;

function a() public {

uint[5] memory arr1 = [uint(0),1,2,3,4];//uint8显示的转换为uint256,否则会报类型错误。
uint[] memory memoryArr;
//storageArr[0] = 12;
//memoryArr[0] = 13; //执行会报VM error: invalid opcode.,原因是数组还没有执行初始化。

storageArr = new uint[](5);
memoryArr = new uint[](5);

storageArr[0] = 12;
memoryArr[0] = 13;
}
}

Memory数组

对于memory的变长数组,不支持修改length属性,来调整数组大小。memory的变长数组虽然可以通过参数灵活指定大小,但一旦创建,大小不可调整。

push方法

变长的storage数组和bytes(不包括string)有一个push()方法。可以将一个新元素附加到数组最后端,返回值为当前长度uint。

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

contract Test {

uint[] arr;

function a() public returns (uint) {
arr.push(1);// 初始化前调用
arr = new uint[](1);
arr[0] = 0;

uint len = arr.push(1);//先数组的最后添加一个元素1,方法返回的是数组的长度

return len;
}
}

memory的数组不可修改,不支持push方法。

多维数组

uint[2][3]在大多数语言中,表示的是两行三列的数组,而Solidity切好相反,它表示的是三行两列的数组。但是访问数组的方法与其他语言一致。

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

contract Test {

uint[2][3] arr = [[1,2],[3,4],[5,6]];

function arr_len() public returns (uint) {

return arr.length; //返回值为3
}
}

固定的字节数组和可变的字节数组

  • bytes和string是一种特殊的数组。bytes类似于byte[],但在外部函数作为参数调用中,会进行压缩打包,更省空间,所以应该尽量使用bytes而不是byte[].
  • bytes0~bytes32 表示创建固定字节大小的数组,不可修改。
  • string是特殊的可变字节数组。可以转换为bytes以通过length获得它的字节长度。也可以通过索引来修改对应的字节内容,通过push方法来增加字节内容。
  • 由于bytes与string,可以自由转换,你可以将字符串s通过bytes(s)转为一个bytes。但需要注意的是通过这种方式访问到的是UTF-8编码的码流,并不是独立的一个个字符。比如中文编码是多字节,变长的,所以你访问到的很有可能只是其中的一个代码点。
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
pragma solidity ^0.4.0;

contract Test {
// 声明一个固定长度的数组,不可修改
bytes9 a = 0x6c697975656368756e;
byte[9] b = [byte(0x6c),0x69,0x79,0x75,0x65,0x63,0x68,0x75,0x6e];

byte[] c = new byte[](10);

// function setAIndex0Byte() public {
// // 错误,不可修改
// a[0] = 0x89;
// }

function setBIndex0Byte() public {
b[0] = 0x89;
}

function setC() public {
for(uint i = 0; i < b.length; i++) {

c.push(b[i]);
c.push(b[i]);
}
}
}

结构体

  • 不能声明一个struct同时将这个struct作为这个struct的一个成员。这个限制是基于结构体的大小必须是有限的。
  • 在函数中,将一个struct赋值给一个局部变量(默认是storage类型),实际是拷贝的引用,所以修改局部变量值时,会影响到原变量。
  • 通常情况下不会考虑使用 uint 变种,因为无论如何定义 uint的大小,Solidity 都会为它保留256位的存储空间。例如,使用 uint8 而不是uint(uint256)不会节省任何 gas。
  • 而如果一个 struct 中有多个 uint,则尽可能使用较小的 uint, Solidity 会将这些 uint 打包在一起,从而占用较少的存储空间。
  • 结构体是不对外可见的(当前只支持internal),所以只可以在当前合约,或合约的子类中使用。包含自定义结构体的函数均需要声明为internal或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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
pragma solidity ^0.4.0;

contract CrowdFunding{
struct Funder{
address addr;
uint amount;
}

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

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

function candidate(address beneficiary, uint goal) returns (uint compaingnID){
// initialize
campaigns[compaingnID++] = Campaign(beneficiary, goal, 0, 0);
}

function vote(uint compaingnID) payable {
Campaign c = campaigns[compaingnID];

//another way to initialize
c.funders[c.funderNum++] = Funder({addr: msg.sender, amount: msg.value});
c.amount += msg.value;
}

function check(uint comapingnId) returns (bool){
Campaign c = campaigns[comapingnId];

if(c.amount < c.goal){
return false;
}

uint amount = c.amount;
// incase send much more
c.amount = 0;
if(!c.beneficiary.send(amount)){
throw;
}
return true;
}
}

字典

定义方式为mapping(_KeyType => _KeyValue)。键的类型允许除映射外的所有类型,如数组,合约,枚举,结构体。值的类型无限制。

在映射表中,我们并不存储键的数据,仅仅存储它的keccak256哈希值,用来查找值时使用。因此,映射并没有长度,键集合(或列表),值集合(或列表)这样的概念。

映射类型,仅能用来定义状态变量,或者是在内部函数中作为storage类型的引用。引用是指你可以声明一个,如var storage mappVal的用于存储状态变量的引用的对象,但你没办法使用非状态变量来初始化这个引用。

可以通过将映射标记为public,来让Solidity创建一个访问器。要想访问这样的映射,需要提供一个键值做为参数。如果映射的值类型也是映射,使用访问器访问时,要提供这个映射值所对应的键,不断重复这个过程。下面来看一个例子:

1
2
3
4
5
6
7
8
contract MappingExample{
mapping(address => uint) public balances;

function update(uint amount) returns (address addr){
balances[msg.sender] = amount;
return msg.sender;
}
}

由于调试时,你不一定方便知道自己的发起地址,所以把这个函数,略微调整了一下,以在调用时,返回调用者的地址。编译上述合同后,可以先调用update(),执行成功后,查看调用信息,能看到你更新的地址,这样再查一下这个地址的在映射里存的值。

如果你想通过合约进行上述调用。

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
pragma solidity ^0.4.0;

//file indeed for compile
//may store in somewhere and import
contract MappingExample{
mapping(address => uint) public balances;

function update(uint amount) returns (address addr){
balances[msg.sender] = amount;
return msg.sender;
}
}

contract MappingUser{

address conAddr;
address userAddr;

function f() returns (uint amount){
//address not resolved!
//tringing
conAddr = hex"0xf2bd5de8b57ebfc45dcee97524a7a08fccc80aef";
userAddr = hex"0xca35b7d915458ef540ade6068dfe2f44e8fa733c";

return MappingExample(conAddr).balances(userAddr);
}
}

映射并未提供迭代输出的方法,可以自行实现一个数据结构。

运算符delete

delete运算符,用于将某个变量重置为初始值。对于整数,运算符的效果等同于a = 0。而对于定长数组,则是把数组中的每个元素置为初始值,变长数组则是将长度置为0。对于结构体,也是类似,是将所有的成员均重置为初始值。delete对于映射类型几乎无影响,因为键可能是任意的,且往往不可知。所以如果你删除一个结构体,它会递归删除所有非mapping的成员。当然,你是可以单独删除映射里的某个键,以及这个键映射的某个值。

需要强调的是delete a的行为更像赋值,为a赋予一个新对象。我们来看看下文的示例:

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
pragma solidity ^0.4.0;

contract DeleteExample {
uint data;
uint[] dataArray;

function f() {
//值传递
uint x = data;
//删除x不会影响data
delete x;

//删除data,同样也不会影响x,因为是值传递,它存的是一份原值的拷贝。
delete data;

//引用赋值
uint[] y = dataArray;

//删除dataArray会影响y,y也将被赋值为初值。
delete dataArray;

//下面的操作为报错,因为删除是一个赋值操作,不能向引用类型的storage直接赋值从而报错
//delete y;
}
}

通过上面的代码,我们可以看出,对于值类型,是值传递,删除x不会影响到data,同样的删除data也不会影响到x。因为他们都存了一份原值的拷贝。而对于复杂类型略有不同,复杂类型在赋值时使用的是引用传递。删除会影响所有相关变量。比如上述代码中,删除dataArray同样会影响到y。由于delete的行为更像是赋值操作,所以不能在上述代码中执行delete y,因为不能对一个storage的引用赋值

类型推断

为了方便,并不总是需要明确指定一个变量的类型,编译器会通过第一个向这个对象赋予的值的类型来进行推断1。

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

函数的参数,包括返回参数,不可以使用var这种不指定类型的方式。

需要特别注意的是,由于类型推断是根据第一个变量进行的赋值。所以代码for (var i = 0; i < 2000; i++) {}将是一个无限循环,因为一个uint8的i的将小于2000。

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

contract Test{
function a() returns (uint){
uint count = 0;
for (var i = 0; i < 2000; i++) {
count++;
if(count >= 2100){
break;
}
}
return count;
}
}

单位

货币单位

一个字面量的数字,可以使用后缀wei,finney,szabo或ether来在不同面额中转换。不含任何后缀的默认单位是wei。如2 ether == 2000 finney的结果是true。

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

contract EthUnit{
uint a;

function f() returns (bool){
if (2 ether == 2000 finney){
return true;
}

return false;
}
}

时间单位(Time Units)

seconds,minutes,hours,days,weeks,years均可做为后缀,并进行相互转换,默认是seconds为单位。

变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)。Unix时间传统用一个32位的整数进行存储,这会导致“2038年”问题,当这个32位的unix时间戳不够用,产生溢出,使用这个时间的遗留系统就麻烦了。所以,如果我们想让我们的 DApp 跑够20年,我们可以使用64位整数表示时间,但为此我们的用户又得支付更多的 gas。真是个两难的设计啊!

如果你需要进行使用这些单位进行日期计算,需要特别小心,因为不是每年都是365天,且并不是每天都有24小时,因为还有闰秒。由于无法预测闰秒,必须由外部的oracle来更新从而得到一个精确的日历库(内部实现一个日期库也是消耗gas的)。

后缀不能用于变量。如果你想对输入的变量说明其不同的单位,可以使用下面的方式。

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

contract DeleteExample{

function nowInSeconds() returns (uint256){
return now;
}

function f(uint start, uint daysAfter) {
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)当前块的时间戳。
  • msg.data (bytes)完整的调用数据(calldata)。
  • msg.gas (uint)当前还剩的gas。
  • msg.sender (address)当前调用发起人的地址,总是存在。
  • msg.sig (bytes4)调用数据的前四个字节(函数标识符)。
  • msg.value (uint)这个消息所附带的货币量,单位为wei。
  • now (uint)当前块的时间戳,等同于block.timestamp
  • tx.gasprice (uint) 交易的gas价格。
  • tx.origin (address)交易的发送者(完整的调用链)

msg的所有成员值,如msg.sender,msg.value的值可以因为每一次外部函数调用,或库函数调用发生变化(因为msg就是和调用相关的全局变量)。如果你想在库函数中,用msg.sender实现访问控制,你需要将msg.sender做为参数(就是说不能使用默认的msg.value,因为它可能被更改)。为了可扩展性的原因,你只能查最近256个块,所有其它的将返回0.

数学和加密函数

  • asser(bool condition):如果条件不满足,抛出异常。
  • addmod(uint x, uint y, uint k) returns (uint):计算(x + y) % k。加法支持任意的精度。但不超过(wrap around?)2**256。
  • mulmod(uint x, uint y, uint k) returns (uint):计算(x * y) % k。乘法支持任意精度,但不超过(wrap around?)2**256。
  • keccak256(…) returns (bytes32):使用以太坊的(Keccak-256)计算HASH值。紧密打包。
  • sha3(…) returns (bytes32):等同于keccak256()。紧密打包。
  • sha256(…) returns (bytes32):使用SHA-256计算HASH值。紧密打包。
  • ripemd160(…) returns (bytes20):使用RIPEMD-160计算HASH值。紧密打包。
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):通过签名信息恢复非对称加密算法公匙地址。如果出错会返回0,附录提供了一个例子1.
  • revert():取消执行,并回撤状态变化。

地址相关

  • .balance (uint256):Address的余额,以wei为单位。
  • .transfer(uint256 amount):发送给定数量的ether,以wei为单位,到某个地址。失败时抛出异常。
  • .send(uint256 amount) returns (bool):发送给定数量的ether,以wei为单位,到某个地址。失败时返回false。
  • .call(...) returns (bool):发起底层的call调用。失败时返回false。
  • .callcode(...) returns (bool):发起底层的callcode调用,失败时返回false。
  • .delegatecall(...) returns (bool):发起底层的delegatecall调用,失败时返回false。

使用send方法需要注意,调用栈深不能超过1024,或gas不足,都将导致发送失败。使用为了保证你的ether安全,要始终检查返回结果。当用户取款时,使用transfer或使用最佳实践的模式2。

合约相关

  • this(当前合约的类型):当前合约的类型,可以显式的转换为Address
  • selfdestruct(address recipt):销毁当前合约,并把它所有资金发送到给定的地址。

进阶

控制语句

不支持switch和goto,支持if,else,while,do,for,break,continue,return,?:。条件判断中的括号不可省略,但在单行语句中的大括号可以省略。

函数调用

内部函数调用

在当前的合约中,函数可以直接调用(内部调用方式),包括也可递归调用。

外部函数调用

表达式this.g(8);和c.g(2)(这里的c是一个合约实例)是外部调用函数的方式。实现上是通过一个消息调用,而不是直接通过EVM的指令跳转。需要注意的是,在合约的构造器中,不能使用this调用函数,因为当前合约还没有创建完成。

其它合约的函数必须通过外部的方式调用。对于一个外部调用,所有函数的参数必须要拷贝到内存中。当调用其它合约的函数时,可以通过选项.value(),和.gas()来分别指定,要发送的ether量(以wei为单位),和gas值。

命名参数调用和匿名函数参数

函数调用的参数,可以通过指定名字的方式调用,但可以以任意的顺序,使用方式是{}包含。但参数的类型和数量要与定义一致。

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

contract C {
function add(uint val1, uint val2) returns (uint) { return val1 + val2; }

function g() returns (uint){
// named arguments
return add({val2: 2, val1: 1});
}
}

省略函数名称

没有使用的参数名可以省略(一般常见于返回值)。这些名字在栈(stack)上存在,但不可访问。

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

contract C {
// omitted name for parameter
function func(uint k, uint) returns(uint) {
return k;
}
}

创建合约实例

一个合约可以通过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 Account{
uint accId;

//construction?
function Account(uint accountId) payable{
accId = accountId;
}
}

contract Initialize{
Account account = new Account(10);

function newAccount(uint accountId){
account = new Account(accountId);
}

function newAccountWithEther(uint accountId, uint amount){
account = (new Account).value(amount)(accountId);
}
}

从上面的例子可以看出来,可以在创建合约中,发送ether,但不能限制gas的使用。如果创建因为out-of-stack,或无足够的余额以及其它任何问题,会抛出一个异常。

赋值

Solidity内置支持元组(tuple),可以同时返回多个结果,也可用于同时赋值给多个变量。

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.0;

contract C {
uint[] data;

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

function g() {
// 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.
(,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.
}
}

作用范围和声明

函数内定义的变量,在整个函数中均可用,无论它在哪里定义,因为Solidity使用了javascript的变量作用范围的规则。与常规语言语法从定义处开始,到当前块结束为止不同。由此,下述代码编译时会抛出一个异常,Identifier already declared。

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
pragma solidity ^0.4.0;

contract ScopingErrors {
function scoping() {
uint i = 0;

while (i++ < 1) {
uint same1 = 0;
}

while (i++ < 2) {
uint same1 = 0;// Illegal, second declaration of same1
}
}

function minimalScoping() {
{
uint same2 = 0;
}

{
uint same2 = 0;// Illegal, second declaration of same2
}
}

function forLoopScoping() {
for (uint same3 = 0; same3 < 1; same3++) {
}

for (uint same3 = 0; same3 < 1; same3++) {// Illegal, second declaration of same3
}
}

function crossFunction(){
uint same1 = 0;//Illegal
}
}

另外的,如果一个变量被声明了,它会在函数开始前被初始化为默认值。所以下述例子是合法的。

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

contract C{
function foo() 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
}
}

随机数

在Solidity中无法安全地生成随机数,Solidity 中最好的随机数生成器是 keccak256 哈希函数,可以这样来生成一些随机数:

1
2
3
4
5
// 生成一个0到100的随机数:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;

这个方法可以被不诚实的节点攻击,假设我们有一个硬币翻转合约——正面你赢双倍钱,反面你输掉所有的钱。假如它使用上面的方法来决定是正面还是反面 (random >= 50 算正面, random < 50 算反面)。如果我正运行一个节点,我可以只对我自己的节点发布一个事务,且不分享它。我可以运行硬币翻转方法来偷窥我的输赢:如果我输了,我就不把这个事务包含进我要解决的下一个区块中去。我可以一直运行这个方法,直到我赢得了硬币翻转并解决了下一个区块,然后获利。

当然,这需要很大的算力来保证自己可以挖矿成功。

异常

可以使用throw来手动抛出一个异常。抛出异常的效果是当前的执行被终止且被撤销(值的改变和帐户余额的变化都会被回退)。异常还会通过Solidity的函数调用向上冒泡(bubbled up)传递。(send,和底层的函数调用call,delegatecall,callcode是一个例外,当发生异常时,这些函数返回false)。捕捉异常是不可能的。

require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行:

1
2
3
4
5
6
7
8
function sayHiToVitalik(string _name) public returns (string) {
// 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序
// (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
// 两字符串的 keccak256 哈希值来进行判断)
require(keccak256(_name) == keccak256("Vitalik"));
// 如果返回 true, 运行如下语句
return "Hi!";
}
1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;

contract Sharer {
function sendHalf(address addr) payable returns (uint balance) {
if (!addr.send(msg.value / 2))
throw; // also reverts the transfer to Sharer
return this.balance;
}
}

用户可以通过下述方式触发一个异常:

  • 调用throw。
  • 调用require,但参数值为false。

通过assert判断内部条件是否达成,require验证输入的有效性。这样的分析工具,可以假设正确的输入,减少错误。这样无效的操作码将永远不会出现。

assert 和 require 区别在于,require 若失败则会返还给用户剩下的 gas, assert 则不会。

合约详解

合约

Solidity中合约有点类似面向对象语言中的类。合约中有用于数据持久化的状态变量(state variables),和可以操作他们的函数。调用另一个合约实例的函数时,会执行一个EVM函数调用,这个操作会切换执行时的上下文,这样,前一个合约的状态变量(state variables)就不能访问了。

合约可以通过Solidity,或不通过Solidity创建。当合约创建时,一个和合约同名的函数(构造器函数)会调用一次,用于初始化。构造器函数是可选的。仅能有一个构造器,所以不支持重载。

如果不通过Solidity,我们可以通过web3.js,使用JavaScript的API来完成合约创建。

当一个文件中有多个contract时,默认将合约名大写的那个合约当做主合约。

构造函数

修改为:

1
2
3
4
5
contract Test {
constructor() {
// ...
}
}

析构函数

1
2
3
4
5
function kill() {
if (owner == msg.sender) { // 检查谁在调用
selfdestruct(owner); // 销毁合约
}
}

可见性和权限控制

Solidity有两种函数调用方式,一种是内部调用,不会创建一个EVM调用(也叫做消息调用),另一种则是外部调用,会创建EVM调用(会发起消息调用)。Solidity对函数和状态变量提供了四种可见性。分别是external,public,internal,private。其中函数默认是public。状态变量默认的可见性是internal。

  • external: 外部函数是合约接口的一部分,所以我们可以从其它合约或通过交易来发起调用。一个外部函数f,不能通过内部的方式来发起调用,(如f()不可以,但可以通过this.f())。外部函数在接收大的数组数据时更加有效。
  • public: 可以在任何地方调用,不管是内部还是外部。对于public类型的状态变量,会自动创建一个访问器(详见下文)。
  • internal:这样声明的函数和状态变量只能通过内部访问。如在当前合约中调用,或继承的合约里调用。需要注意的是不能加前缀this,前缀this是表示通过外部方式访问。
  • private:私有函数和状态变量仅在当前合约中可以访问,在继承的合约内,不可访问。private修饰的函数名一般以”_”开头.

备注:

  • internal 和 private 类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。
  • 所有在合约内的东西对外部的观察者来说都是可见,将某些东西标记为private仅仅阻止了其它合约来进行访问和修改,但并不能阻止其它人看到相关的信息。
  • 可见性的标识符的定义位置,对于state variable是在类型后面,函数是在参数列表和返回关键字中间。来看一个定义的例子:
1
2
3
4
5
6
7
pragma solidity ^0.4.0;

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

在下面的例子中,D可以调用c.getData()来访问data的值,但不能调用f。合约E继承自C,所以它可以访问compute函数。

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.0;

contract C {
uint private data;

function f(uint a) private returns(uint b) { return a + 1; }
function setData(uint a) { 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() {
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() {
C c = new C();
uint val = compute(3, 5); // acces to internal member (from derivated to parent contract)
}
}

访问函数

编译器为自动为所有的public的状态变量创建访问函数。下面的合约例子中,编译器会生成一个名叫data的无参,返回值是uint的类型的值data。状态变量的初始化可以在定义时完成。

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


contract C{
uint public c = 10;
}

contract D{
C c = new C();

function getDataUsingAccessor() returns (uint){
return c.c();
}
}

函数修饰符

modifier 可以用来改变一个函数的行为。比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约重写(override)。下面我们来看一段示例代码:

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
pragma solidity ^0.4.0;

contract owned {
function owned() { 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 {
if (msg.sender != owner)
throw;
// 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() 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) { 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() payable costs(price) {
registeredAddresses[msg.sender] = true;
}

function changePrice(uint _price) onlyOwner {
price = _price;
}
}
  • 修改器可以被继承,使用将modifier置于参数后,返回值前即可。
  • 特殊_表示使用修改符的函数体的替换位置。
  • 从合约Register可以看出全约可以多继承,通过,号分隔两个被继承的对象。
  • 修改器也是可以接收参数的,如priced的costs。

使用修改器实现的一个防重复进入的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.4.0;
contract Mutex {
bool locked;
modifier noReentrancy() {
if (locked) throw;
locked = true;
_;
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() noReentrancy returns (uint) {
if (!msg.sender.call()) throw;
return 7;
}
}

例子中,由于call()方法有可能会调回当前方法,修改器实现了防重入的检查。如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。需要注意的是,在Solidity的早期版本中,有修改器的函数,它的return语句的行为有些不同。在修改器中和函数体内的显式的return语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的”_”后继续执行。修改器的参数可以是任意表达式。在对应的上下文中,所有的函数中引入的符号,在修改器中均可见。但修改器中引入的符号在函数中不可见,因为它们有可能被重写。

常量

常量

状态变量可以被定义为constant,常量。这样的话,它必须在编译期间通过一个表达式赋值。赋值的表达式不允许:1)访问storage;2)区块链数据,如now,this.balance,block.number;3)合约执行的中间数据,如msg.gas;4)向外部合约发起调用。也许会造成内存分配副作用表达式是允许的,但不允许产生其它内存对象的副作用的表达式。内置的函数keccak256,keccak256,ripemd160,ecrecover,addmod,mulmod可以允许调用,即使它们是调用的外部合约。

允许内存分配,从而带来可能的副作用的原因是因为这将允许构建复杂的对象,比如,查找表。虽然当前的特性尚未完整支持。

编译器并不会为常量在storage上预留空间,每个使用的常量都会被对应的常量表达式所替换(也许优化器会直接替换为常量表达式的结果值)。

不是所有的类型都支持常量,当前支持的仅有值类型和字符串。

新版本中:

  • view: 把函数定义为 view, 意味着它只能读取数据不能更改数据
  • pure: pure 函数表明这个函数不访问应用里的数据
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");
}

常函数

函数也可被声明为常量,这类函数将承诺自己不修改区块链上任何状态。

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

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

访问器(Accessor)方法默认被标记为constant。当前编译器并未强制一个constant的方法不能修改状态。但建议大家对于不会修改数据的标记为constant。

回退函数

每一个合约有且仅有一个没有名字的函数。这个函数无参数,也无返回值。如果调用合约时,没有匹配上任何一个函数(或者没有传哪怕一点数据),就会调用默认的回退函数。此外,当合约收到ether时(没有任何其它数据),这个函数也会被执行。在此时,一般仅有少量的gas剩余,用于执行这个函数(准确的说,还剩2300gas)。所以应该尽量保证回退函数使用少的gas。

下述提供给回退函数可执行的操作会比常规的花费得多一点。

  • 写入到存储(storage)
  • 创建一个合约
  • 执行一个外部(external)函数调用,会花费非常多的gas
  • 发送ether

请在部署合约到网络前,保证透彻的测试你的回退函数,来保证函数执行的花费控制在2300gas以内。一个没有定义一个回退函数的合约。如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。下面来看个例子:

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.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() { x = 1; }
uint x;
}


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


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

// The following call will fail, reject the
// Ether and return false:
test.send(2 ether);
}
}

payable

1
2
3
4
5
6
7
8
contract OnlineStore {
function buySomething() external payable {
// 检查以确定0.001以太发送出去来运行函数:
require(msg.value == 0.001 ether);
// 如果为真,一些用来向函数调用者发送数字内容的逻辑
transferThing(msg.sender);
}
}

提现

在发送以太之后,它将被存储进合约的以太坊账户中,并冻结在哪里,可以写一个函数来从合约中提现以太,类似这样:

1
2
3
4
5
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
owner.transfer(this.balance);
}
}

事件

事件是合约和区块链通讯的一种机制。前端应用“监听”某些事件,并做出反应。(触发事件需要添加emit关键字)

例子:

1
2
3
4
5
6
7
8
9
// 这里建立事件
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
uint result = _x + _y;
//触发事件,通知app
emit IntegersAdded(_x, _y, result);
return result;
}

app前端可以监听这个事件,JavaScript 实现如下:

1
2
3
YourContract.IntegersAdded(function(error, result) { 
// 干些事
}

继承

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
pragma solidity ^0.4.0;

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


// 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) returns (address adr);
}


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


// 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() {
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) {
if (msg.sender == owner) info = newInfo;
}

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

uint info;
}

基类构造器的方法

派生的合约需要提供所有父合约需要的所有参数,所以用两种方式来做,见下面的例子:

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

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


contract Derived is Base(7) {
function Derived(uint _y) Base(_y * _y) {
}
}

或者直接在继承列表中使用is Base(7),或像修改器(modifier)使用方式一样,做为派生构造器定义头的一部分Base(_y * _y)。第一种方式对于构造器是常量的情况比较方便,可以大概说明合约的行为。第二种方式适用于构造的参数值由派生合约的指定的情况。在上述两种都用的情况下,第二种方式优先(一般情况只用其中一种方式就好了)。

继承有相同名字的不同类型成员

当继承最终导致一个合约同时存在多个相同名字的修改器或函数,它将被视为一个错误。同新的如果事件与修改器重名,或者函数与事件重名都将产生错误。作为一个例外,状态变量的getter可以覆盖一个public的函数。

抽象(Abstract Contracts)

抽象函数是没有函数体的的函数。如下:

1
2
3
4
5
pragma solidity ^0.4.0;

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

这样的合约不能通过编译,即使合约内也包含一些正常的函数。但它们可以做为基合约被继承。

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

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

function getContractName() returns (string){
return "Feline";
}
}


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

如果一个合约从一个抽象合约里继承,但却没实现所有函数,那么它也是一个抽象合约。

接口

接口与抽象合约类似,与之不同的是,接口内没有任何函数是已实现的,同时还有如下限制:

  • 不能继承其它合约,或接口。
  • 不能定义构造器
  • 不能定义变量
  • 不能定义结构体
  • 不能定义枚举类
  • 其中的一些限制可能在未来放开。

接口基本上限制为合约ABI定义可以表示的内容,ABI和接口定义之间的转换应该是可能的,不会有任何信息丢失。接口用自己的关键词表示:

1
2
3
interface Token {
function transfer(address recipient, uint amount);
}

合约可以继承于接口,因为他们可以继承于其它的合约。

Gas优化

  • 结构体中尽量使用uint的少位数,且考虑字节对其
  • “view” 函数不花 “gas”,这是因为 view 函数不会真正改变区块链上的任何数据。如果一个 view 函数在另一个函数的内部被调用,而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。所以标记为 view 的函数只有在外部调用时才是免费的(当玩家从web.js外部调用)。
  • 在大多数编程语言中,遍历大数据集合都是昂贵的。但是在 Solidity 中,使用一个标记了external view的函数,遍历比 storage 要便宜太多,因为 view 函数不会产生任何花销。
  • 为了降低成本,不到万不得已,避免将数据写入存储。这也会导致效率低下的编程逻辑:比如每次调用一个函数,都需要在 memory(内存) 中重建一个数组,而不是简单地将上次计算的数组给存储下来以便快速查找。

智能合约升级

将合约进行分离,先部署数据合约,部署后不变。业务合约调用数据合约读取修改数据。保证数据类型不变的情况下,可以对业务合约进行修改,新增。之后部署新的业务合约,废除旧的业务合约。

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
pragma solidity ^0.4.18;

contract DataContract {
mapping (address => uint256) public balanceOf;
mapping (address => bool) accessAllowed;

function DataContract() public {
accessAllowed[msg.sender] = true;
}

function setBlance(address _address,uint256 v) public {
balanceOf[_address] = v;
}

modifier platform() {
require(accessAllowed[msg.sender] == true);
_;
}

function allowAccess(address _addr) platform public {
accessAllowed[_addr] = true;
}

function denyAccess(address _addr) platform public {
accessAllowed[_addr] = false;
}
}

contract ControlContract {

DataContract dataContract;

function ControlContract(address _dataContractAddr) public {
dataContract = DataContract(_dataContractAddr);
}

function addTen(address addr) public returns (uint){
return dataContract.balanceOf(addr) + 11;
}
}

部署方法如下:

  1. 先部署DataContract合约。
  2. 使用DataContract合约地址作为部署ControlContract合约的参数。
  3. 用ControlContract合约地址作为参数调用DataContract合约的allowAccess方法。

如果需要更新控制合约(如修复了addTen)则重新执行第2-3步,同时对老的控制合约执行denyAccess()。

代币

一个代币在以太坊上就是一个遵循一些共同规则的智能合约:以太坊代币标准(ERC-Token Standard)。建立在以太坊网络上的区块链项目代币,需要遵从以下几种代币标准:ERC-20,ERC-223,ERC-621,ERC-721,ERC-827。其中 ERC 是 Ethereum Request for Comments 的简称。

ERC-20

这是最广泛被大家认可的一种代币形式,简单的列举一些通用的标准函数:

  • function totalSupply() 定义 Token 的总量;
  • function balanceOf(address tokenOwner) 显示用户账户余额;
  • function allowance(address tokenOwner, address spender) 返回剩余金额,显示 address spender 能从 address tokenOwner 里提取的数量;
  • function transfer(address to, uint tokens) 转移对应的金额到指定地址;
  • function approve(address spender, uint tokens)  returns (bool success) 允许  address spender 提取部分 Token;
  • function transferFrom(address from, address to, uint tokens) returns (bool success) 从一个地址转移 token 到另一个地址;

拥有以上所有必要的函数实现我们称为兼容 ERC-20 标准,但在具体实现中会做一些扩展,比如 ERC-223。

ERC-223

这个标准支持所有 ERC-20 的函数、智能合约以及服务,并解决了一些 ERC-20 的缺陷,比如说:在 ERC-20 标准下如果你输入了错误的收款地址,你转账的费用可能会永远丢失,但在 ERC-223 里这个问题被避免了,同时在这个标准下你需要消耗的 GAS 费用只有 ERC-20 的一半。

ERC-621

ERC-621 也是一个基于 ERC-20 升级的标准,解决了 ERC-20 不允许 Token 总量更改的问题,不过为了解决这个问题,ERC-621 增加了两种新的函数:

increaseSupply 和 decreaseSupply

ERC-827

这个标准比 ERC-20 更加灵活,除用于转账外,还可以转移数据和让第三方在获取用户允许的情况下为用户转账。

ERC-721

ERC-721 与 ERC-20 有很大的区别,如果说 ERC-20 与 ERC-223,ERC-621 能够在使用中自由转换的话,ERC-721 是不可与 ERC-20 Token 互相转换的,因为 ERC-721 拥有唯一性。这种 Token 依然可以在交易所里交易,只不过无法分割是一个独立的整体。

1
2
3
4
5
6
7
8
9
10
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 _tokenId);

function balanceOf(address _owner) public view returns (uint256 _balance);
function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256 _tokenId) public;
function approve(address _to, uint256 _tokenId) public;
function takeOwnership(uint256 _tokenId) public;
}

ERC721 规范有两种不同的方法来转移代币:

  1. 第一种方法是代币的拥有者调用transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId。
  2. 第二种方法是代币拥有者首先调用 approve,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个 mapping (uint256 => address) 里。然后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。
    • 所有者,用新主人的 address 和你希望他获取的 _tokenId 来调用 approve
    • 新主人用 _tokenId 来调用 takeOwnership,合约会检查确保他获得了批准,然后把代币转移给他。

Ownable

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
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address public owner;

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
function Ownable() public {
owner = msg.sender;
}

/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(msg.sender == owner);
_;
}

/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
require(newOwner != address(0));
OwnershipTransferred(owner, newOwner);
owner = newOwner;
}
}

SafeMath

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
library SafeMath {

function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}

function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}

function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}

function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}

SafeMath 库允许使用 using 关键字,它可以自动把库的所有方法添加给一个数据类型:

1
2
3
4
5
using SafeMath for uint;
// 这下我们可以为任何 uint 调用这些方法了
uint test = 2;
test = test.mul(3); // test 等于 6 了
test = test.add(5); // test 等于 11 了

所以简而言之, SafeMath 的 add, sub, mul, 和 div 方法只做简单的四则运算,然后在发生溢出或下溢的时候抛出错误。