一.ABI
ABI是和Ethereum生态系统的合约进行交互的标准方式,所有合约的调用都是通过ABI.一个函数调用时的call data中前四个bytes指定了哪个函数被调用,从第五个字节开始则是函数的编码.
function selector
所有合约交易的function selector都是根据methodName(type01,type02,...typeN)
的keccak256前八位来决定的,例如例子的Erc20规范转账,前四个bytes一定是a9059cbb
String encF = Hash.sha3String("transfer(address,uint256)");
//0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
arugment encoding
不同类型有不同的编码规则,定长类型和不定长类型的编码规则也是不同的.
这些是不定长的类型:
bytes
string
-
T[]
:T
可以是任何类型 -
T[k]
T
是任何类型的动态长度类型, 并且k >= 0
-
(T1,...,Tk)
ifTi
is dynamic for some1 <= i <= k
其他类型都是定长的.
example
给定一个合约:
pragma solidity ^0.4.16;
contract Foo {
function bar(bytes3[2]) public pure {}
function baz(uint32 x, bool y) public pure returns (bool r) { r = x > 32 || y; }
function sam(bytes, bool, uint[]) public pure {}
}
function baz(uint32 x, bool y)
因此,对于我们的合约Foo
,如果我们想传入69
和true
来调用函数baz
,我们总共会传入68个bytes,拆开来看:
-
0xcdcd77c0
: Method ID. 这是作为baz(uint32,bool)
签名的ASCII形式的Keccak hash的前四个字节. -
0x0000000000000000000000000000000000000000000000000000000000000045
:第一个参数,一个uint32类型的69
, 用0填充到 32 bytes -
0x0000000000000000000000000000000000000000000000000000000000000001
: 第二个参数 - booleantrue
, 用0填充到 32 bytes
拼一起:
0xcdcd77c000000000000000000000000000000000000000000000000000000000000000450000000000000000000000000000000000000000000000000000000000000001
这个函数返回单一参数bool
.如果它返回了false
,它的输出会是一个单一的byte数组0x0000000000000000000000000000000000000000000000000000000000000000
,一个bool.
function bar(bytes3[2])
如果我们想传入["abc","def"]
来调用bar
,我们一共会传入68bytes,细分如下:
-
0xfce353f6
: the Method ID. 从bar(bytes3[2])
的签名而来. -
0x6162630000000000000000000000000000000000000000000000000000000000
:第一个参数,bytes3
类型的值"abc"
(left-aligned). -
0x6465660000000000000000000000000000000000000000000000000000000000
: 第二个参数,bytes3
类型的值"def"
(left-aligned).
拼一起:
0xfce353f661626300000000000000000000000000000000000000000000000000000000006465660000000000000000000000000000000000000000000000000000000000
function sam(bytes, bool, uint[])
如果我们想传入参数 "dave"
, true
和 [1,2,3]
来调用 sam
, 我们总共会传入292 bytes, 细分如下:
-
0xa5643bf2
: the Method ID. This is derived from the signaturesam(bytes,bool,uint256[])
. Note thatuint
会被替换成规范的表示-uint256
. -
0x0000000000000000000000000000000000000000000000000000000000000060
: 第一个参数的数据部分所在位置 (dynamic type), 从参数区块开始位置开始计算,以字节为单位. 这种情况下,0x60
. -
0x0000000000000000000000000000000000000000000000000000000000000001
: 第二个参数: boolean true. -
0x00000000000000000000000000000000000000000000000000000000000000a0
: 第三个参数的数据部分所在位置 (dynamic type),以字节为单位. In this case,0xa0
. -
0x0000000000000000000000000000000000000000000000000000000000000004
: 第一个参数的数据位置部分, 它以元素中字节数组的长度开始, in this case, 4. -
0x6461766500000000000000000000000000000000000000000000000000000000
: 第一个参数的内容: UTF-8 (equal to ASCII in this case) 编码 的"dave"
, 右边填充至 32 bytes. -
0x0000000000000000000000000000000000000000000000000000000000000003
: 第三个参数的数据部分,他以数组的长度开始, in this case, 3. -
0x0000000000000000000000000000000000000000000000000000000000000001
: 第三个参数的第一个条目. -
0x0000000000000000000000000000000000000000000000000000000000000002
: 第三个参数的第二个条目. -
0x0000000000000000000000000000000000000000000000000000000000000003
: 第三个参数的第三个条目.
拼一起:
0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003
dynamic types
调用一个有签名的函数f(uint,uint32[],bytes10,bytes)
传入值(0x123,[0x456,0x789],"1234567890","Hello, world!")
会以下面这种方式编码:
调用一个有签名的函数f(uint,uint32[],bytes10,bytes)
传入值(0x123,[0x456,0x789],"1234567890","Hello, world!")
会以下面这种方式编码:
先获取sha3("f(uint256,uint32[],bytes10,bytes)")
的前四个字节,我们拿到0x8be65246
.然后我们对四个参数的头部部分进行编码.对于静态类型uint256
和bytes10
,他们会直接将他们自己穿进去,而对于动态类型uint32[]
和bytes
来说,我们将它们的数据区域的开始部分以字节为单位进行偏移,从编码后的位置开始数(例如:函数签名的前四个字节不纳入位置).将会编码成:
-
0x0000000000000000000000000000000000000000000000000000000000000123
(0x123
左填充到 32 bytes) -
0x0000000000000000000000000000000000000000000000000000000000000080
(第二个参数数据部分的偏移位置,从开始计算, 4*32 bytes, 刚好是头部部分的大小) -
0x3132333435363738393000000000000000000000000000000000000000000000
("1234567890"
,右填充到32bytes) -
0x00000000000000000000000000000000000000000000000000000000000000e0
(第四个参数数据部分的偏移位置 = 第一个动态参数数据部分的偏移位置的开始 + 第一个动态参数数据部分的大小 = 432 + 332 (参照下面))
在这之后,第一个动态参数的数据部分[0x456,0x789]
如下所示:
-
0x0000000000000000000000000000000000000000000000000000000000000002
(数组元素的个数, 2) -
0x0000000000000000000000000000000000000000000000000000000000000456
(第一个元素) -
0x0000000000000000000000000000000000000000000000000000000000000789
(第二个元素)
最后,我们将第二个动态参数的数据"Hello, world!"
编码出来:
-
0x000000000000000000000000000000000000000000000000000000000000000d
(元素的大小 (bytes in this case): 13) -
0x48656c6c6f2c20776f726c642100000000000000000000000000000000000000
(`"Hello, world!" 向右填充到32bytes)
全部放在一起, (0x123,[0x456,0x789],"1234567890","Hello, world!")
编码出来的内容是 (为了易读性,函数选择器后每32-bytes都会另起一行):
0x8be65246 //函数选择器 (Method ID)
0000000000000000000000000000000000000000000000000000000000000123 //uint 的0x123
0000000000000000000000000000000000000000000000000000000000000080 //uint32[] 的内容偏移位置
3132333435363738393000000000000000000000000000000000000000000000 //string的utf-8转hex
00000000000000000000000000000000000000000000000000000000000000e0 //bytes 的偏移位置
0000000000000000000000000000000000000000000000000000000000000002 //第二个参数(数组)的长度
0000000000000000000000000000000000000000000000000000000000000456 //第二个参数的第一个元素
0000000000000000000000000000000000000000000000000000000000000789 //第二个参数的第二个元素
000000000000000000000000000000000000000000000000000000000000000d //第四个参数的长度
48656c6c6f2c20776f726c642100000000000000000000000000000000000000 //第四个参数的bytes转hex
一个函数中如果包含定长类型和动长类型,ABI的生成总结下来就是:
- method ID
- 依据参数顺序依次进行编码,如果是定长类型,直接encode,如果是动长类型,存入其开始的偏移位置
- 依次展示动长类型的长度以及他们的数据编码
events
编码规则和function ABI类似,会由以下部分组成:
- 合约的地址(address)
- 最多四个的topic(角标为0的为
keccak(EVENT_NAME+("+EVENT_ARGS.map(canonical_type_of).join(",")+")")
,从1-3是定义event
时添加了indexed
的参数,例如event Transfer(address indexed from, address indexed to, uint256 value);
中的from
和to
) - 长度不定的二进制数据(例如上面的
value
)
例如一个标准Erc20转账的交易:
Function: transfer(address _to, uint256 _value)
MethodID: 0xa9059cbb
[0]: 000000000000000000000000320bc367d5c1ebe5695f8f2544db46d990d5241c
[1]: 000000000000000000000000000000000000000000000002a802f8630a240000
会生成以下Event:
Address 0xc3af5103551287cfc8f12d7bfe208e0c2c3c3ff1
Name Transfer (index_topic_1 address from, index_topic_2 address to, uint256 value)
Topics
[0] 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
[1] 0x0000000000000000000000007ab2b50a62006c0e87b527228761b8501c798c6e
[2] 0x000000000000000000000000320bc367d5c1ebe5695f8f2544db46d990d5241c
Data
000000000000000000000000000000000000000000000002a802f8630a240000
对于event和function来说,他们的ABI都会将省略类型补成官方定义的类型
二.storage or memory
官方定义中EVM有三个可以存储的区域:
第一个是"storage",其中包含所有合同状态变量。每个合同都有自己的存储空间,它在函数调用之间是持久的,使用起来非常昂贵。 第二个是"memory",这用于保存临时值。它在(外部)函数调用之间被擦除,并且使用起来更便宜。 第三个是"stack",用于保存小的局部变量。它几乎可以免费使用,但只能保留有限数量的值。 最重要的是,如果您例如在函数调用中传递此类变量,则如果它们可以保留在memory中或保留在storage中,则不会复制它们的数据。
-
storage
和memory
关键字用于在存储和存储器分别引用数据。 - 合同存储在合同构建期间预先分配,不能在函数调用中创建。毕竟,如果要保留函数,在函数存储中创建新变量没有多大意义。
- 在合同构建期间不能分配内存,而是在函数执行中创建。合同状态变量始终在存储中声明。同样,拥有不能持久的状态变量也没有意义。
- 将
memory
引用数据分配给storage
引用变量时,我们将数据从内存复制到存储。没有创建新存储。 - 将
storage
引用数据分配给memory
引用变量时,我们将数据从存储复制到内存。分配了新内存。 - 当
storage
在函数通过查找创建本地变量,它简单地引用已经分配上存储数据。没有创建新存储。
以下面的代码作为示例,先看看getter:
// 不设置成view函数来估算gas
function getUsingStorage(uint _itemIdx) public returns (uint){
Item storage item = items[_itemIdx];
return item.units;
}
// 不设置成view函数来估算gas
function getUsingMemory(uint _itemIdx) public returns (uint){
Item memory item = items[_itemIdx];
return item.units;
}
两个函数都返回相同的结果,但是getUsingMemory
创建一个新变量所以使用了更多的gas :
// getUsingStorage
"gasUsed": 21849
// getUsingMemory
"gasUsed": 22149
另外我们再看看setter:
function addItemUsingStorage(uint _itemIdx, uint _units) public{
Item storage item = items[_itemIdx];
item.units += _units;
}
function addItemUsingMemory(uint _itemIdx, uint _units) public{
Item memory item = items[_itemIdx];
item.units += _units;
}
只有addItemUsingStorage
修改了状态变量(意味着消费了更多gas):
// addItemUsingStorage
// `units` changes in `items`
"gasUsed": 27053,
// addItemUsingMemory
// `units` does not change in `items`
"gasUsed": 22287
作为结论:
-
memory
与storage
指定data location
变量引用的内容 -
storage
不能在函数中新创建。storage
函数中任何引用的变量总是指在合同存储(状态变量)上预先分配的一段数据。函数调用后任何突变都会持续存在。 -
memory
只能在函数中新创建。它可以是新实例化的复杂类型,如array / struct(例如通过new int[...]
),也可以从storage
引用的变量中复制。 - 由于引用是通过函数参数在内部传递的,请记住它们默认是
memory
,如果变量是在storage
中,它将创建一个副本,任何修改都不会持久。(这条有问题吧)
参考
三.细节及技巧
命名勿用关键字
除了目前已经存在的关键字(函数声明,事件声明,变量声明的关键字)以外,以下关键字也请保留
abstract
, after
, case
, catch
, default
, final
, in
, inline
, let
, match
, null
, of
, relocatable
, static
, switch
, try
, type
, typeof
.
storage中变量的存储位置
当使用小于32 bytes的元素时,你合约用的gas可能会更高.这是因为EVM每次操作都是使用32bytes.因此,如果一个元素比32 bytes小,EVM必须使用更多的操作去减小这个元素的大小到它需要的大小.
最后,EVM存储中有一个概念叫存储槽,为了让EVM去优化,保证你尝试去给你的存储变量以及struct
的成员变量排序,以让他们能被紧紧地打包.例如:将你声明的uint128,uint256, uint128
替换为 uint128, uint128, uint256
,前者会使用3个存储槽而后者只会使用2个.
错误处理(作为事务)
为了让合约函数的操作不影响state variables,我们需要考虑到revert.
在函数开始时就需要进行判断条件并revert的,能使用require
就使用它(如果是经常需要判断的情况,请抽取到modifier
中).
在函数执行时需要根据条件进行revert的,使用assert
而不是if
,assert
能直接帮我们使事务无效,并进行revert.
如果有异常发生,assert会消耗当前调用函数的所有gas,而require从Metropolis版本开始不会消耗任何gas
变量声明
默认值
所有声明出来的变量都有一个初始默认值,是它们这个类型对应的bytes32的0,例如:bool
的默认值是false
,uint
和int
的默认值是0
.
对于静态长度的数组以及从byte1
到byte32
这些类型来说,它们存储的每个元素的默认值都对应它们类型的0.
对于一个动态长度的数组,bytes
以及string
来说,默认值是空数组(长度为0的数组)或空字符串.
范围
Solidity继承了JS的范围规则—变量声明后它就属于整个函数而不是当前代码块.例如:
pragma solidity ^0.4.16;
contract ScopingErrors {
function scoping() public {
uint i = 0;
while (i++ < 1) {
uint same1 = 0;
}
while (i++ < 2) {
uint same1 = 0;// 非法, 第二次声明 same1
}
}
function minimalScoping() public {
{
uint same2 = 0;
}
{
uint same2 = 0;// 非法, 第二次声明 same2
}
}
function forLoopScoping() public {
for (uint same3 = 0; same3 < 1; same3++) {
}
for (uint same3 = 0; same3 < 1; same3++) {// 非法, 第二次声明 same3
}
}
}
但是,从v0.5.0
开始,Solidty的变量范围会更改成遵循C99标准,变量的可见性在它声明出来直到{}
代码块结束.作为此规则的一个例外,在for循环的初始化部分中声明的变量只有在for循环结束前可见(和大多数语言一致).
下面这个例子不会产生任何警告,因为这两个变量虽然有同样的名字但是范围不相交.在非0.5.0的模式,他们有同样的scope(在function minimalScoping
中)并且不会编译通过.
pragma solidity ^0.4.0;
pragma experimental "v0.5.0";
contract C {
function minimalScoping() pure public {
{
uint same2 = 0;
}
{
uint same2 = 0;
}
}
}
Getter
所有state variables
声明为public
时,编译器都会为他们自动生成getter
函数,该函数与state variables
的名称一致.
对于普通类型(非数组,非mapping)的元素来说,如果声明成public
,直接调用与variables
名称相同的函数即可获取它的值,
对于数组来说,会生成variableName(uint index)
的函数来获取对应角标的值:
例如:
pragma solidity ^0.4.0;
contract Getter{
uint public x =20;
uint[] public arr = [10,20,30];
mapping (uint => uint) public map;
constructor() public{
map[3] = 13;
map[4] = 14;
}
}
//编译器自动生成的getter
function x() view public{
return x;
}
function arr(uint index) view public returns (uint){
return arr[index];
}
function map(uint key) view public returns (uint){
return map[key];
}
再看编译器会如何为一个复杂结构的变量生成getter:
pragma solidity ^0.4.0;
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
}
mapping (uint => mapping(bool => Data[])) public datas;
}
//例如上面的public datas,会自动生成如下的getter:
function datas(uint arg1, bool arg2, uint arg3) public returns (uint a, bytes3 b) {
a = datas[arg1][arg2][arg3].a;
b = datas[arg1][arg2][arg3].b;
}
提供Fallback Function
每个合约可以有并且只有一个没有名称的函数,该函数就是Fallback Function(备用函数),它会在其他函数都匹配不上所给出的method selector时被调用(或者没有提供任何数据时).
这个函数可以用于函数接受Ether,但它如果不被标记为payable
的话是无法接受Ether的,记得给每个Fallback Function提供payable
标记.
函数重载
在Solidity中也有函数重载的概念(函数名称相同的参数类型不同或数量),例如:
pragma solidity ^0.4.16;
contract A {
function f(uint _in) public pure returns (uint out) {
out = 1;
}
function f(uint _in, bytes32 _key) public pure returns (uint out) {
out = 2;
}
}
如果两个外部可见函数中的Solidity类型不同但是外部类型相同,则会有错(见下面示例:B
和address
虽然在Solidity中声明出来是不同的,但实际上是一致的).
// 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 {
}
两个f
函数重载最终都接受了ABI的address类型,尽管他们在Solidity中是不同类型的.
注意
函数的返回参数不能作为重载的考虑因素.
主要类型相同的参数尽量不要重载,可能会引起一些不必要的错误,例如:
pragma solidity ^0.4.16;
contract A {
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
}
调用f(50)
会创建一个类型异常,因为50
可以被隐式转换为uint8
和uint256
.
合约继承的构造函数参数传递
所有基本合约的构造函数调用需要遵循以下的线性规则.如果base构造函数有参数,派生合约需要指定这些参数.能以以下两种方式来完成:
pragma solidity ^0.4.22;
contract Base {
uint x;
//base contract的有参构造
constructor(uint _x) public { x = _x; }
}
//在声明合约的时候指定base contract的构造参数
contract Derived1 is Base(7) {
constructor(uint _y) public {}
}
//在声明构造函数的时候将base contract constructor写上
contract Derived2 is Base {
constructor(uint _y) Base(_y * _y) public {}
}
第一种方式:直接在声明继承时传入.如果构造函数的参数是常量,那么使用这种更方便.第二种方式:像modifier一样直接声明在constructor
后面.如果base contract构造函数的参数依赖于derived contract构造函数的参数,那需要使用第二种.参数一定需要在继承列表或以修饰符风格写在派生构造函数后.同时在这两个地方指定参数是错误的.
如果派生合约没有将所有base contracts' constructors的参数指定出来,那么它将会是abstract的.
多继承同名错误
如果一个合约多继承中出现了function和modifier同名,这会视为是错误.这个错误同样会因为一个event和一个modifier名字一样引起,一个function和event也是不能同名的.作为一个例外,一个state variable的getter可以override一个pulic function.
Mapping
Mapping可以被看成hash tables,但EVM中的Mapping是被虚拟初始化出来的,并且拥有所有可能的key,以及这些key都映射到了其字节表示全为零的值.(ps:mapping中的key并不保存key本身,而是其keccak256
的hash).因此,mapping
并没有长度以及类似key/value这种形式的set
.
mapping只允许存在在state variables(或者内部函数的storage引用中).
mapping
是不支持遍历的,但是可以基于它创建一个可遍历的数据结构,例如 [iterable mapping](iterable mapping),另外也可以考虑自己设计一个与数组相关的数据结构来记录key以获取value.
return struct
在合约中,内部函数可以返回struct,外部函数(或public函数)不能返回struct,如果想要返回,可以返回一组值,如下面示例:
contract MyContract {
struct MyStruct {
string str;
uint i;
}
MyStruct myStruct;
constructor() public{
myStruct = MyStruct("foo", 1);
}
//internal function可以返回struct
function returnStruct() internal view returns (MyStruct){
return myStruct;
}
//目前版本public函数不能返回struct,但是可以使用`pragma experimental ABIEncoderV2;`让编译通过
//function returnStructPublic() public view returns (MyStruct){
// return myStruct;
//}
function myMethod() external view returns (string, uint) {
return (myStruct.str, myStruct.i);
}
}
改变数组长度
我们可以使用arrayName.length = uint
来改变storage中一个动态数组的长度,如果改变长度的时候报了一个错误:lvalue
,我们可能因为下面两个原因让这个错误出现:
1.在对memory中的array进行resize;
2.对一个非动态长度的数组进行resize;
int8[] memory memArr; // Case 1
memArr.length++; // illegal(memory的动态长度array不能resize)
int8[5] storageArr; // Case 2
somearray.length++; // legal
int8[5] storage storageArr2; // Explicit case 2
somearray2.length++; // legal