区块链技术全面剖析:Solidity合约编写

一、特别点

1.版本声明,pragma solidity ^0.4.0;版本要高于0.4才可以编译

2. 函数,分为internalexternal函数,可以直接调用f(),或者使用this.f()。但两者有一个区别。前者是通过internal的方式在调用,而后者是通过external的方式在调用。请注意,这里关于this的使用与大多数语言相背。出参在returns关键字后定义,参数名字可以省略。其它合约的函数必须通过外部的方式调用。对于一个外部调用,所有函数的参数必须要拷贝到内存中。当调用其它合约的函数时,可以通过选项.value()和.gas()来分别指定要发送的ether量(以wei为单位)和gas值。函数的参数可以通过指定名字的方式以任意的顺序来调用,使用方式是{}包含。

3. 结构struct,在函数中将一个struct赋值给一个局部变量(默认是storage类型),实际是拷贝的引用,所以修改局部变量值时,会影响到原变量。

4. 数据 array,对storage的数组来说,元素类型可以是任意的,类型可以是数组,映射类型,数据结构等。但对于memory的数组来说。如果函数是对外可见的,那么函数参数不能是映射类型的数组,只能是支持ABI的类型。如一个类型为uint的数组长度为5的变长数组,可以声明为uint[][5] x ,注意多维数组的长度声明是反的,如果当前在外部函数中,则不能使用多维数组。可使用new关键字创建一个memory的数组,例子:uint[] memory a = new uint[](7);storage的变长数组可以通过给.length赋值调整数组长度。memory的变长数组不支持。

5.映射 mappings,被视作为一个哈希表,其中所有可能的键已被虚拟化的创建,被映射到一个默认值(二进制表示的零)。并没有长度,并未提供迭代输出的方法。

6. 字节数组,固定长度bytes1~bytes32,支持序号的访问,length属性;不定长字节数组bytes用来存储任意长度的字节数据,string用来存储任意长度的UTF-8编码的字符串数据。string类似bytes,但不提供长度和按序号的访问方式。

7. 十六进制字面量,hex”001122FF”、货币字面量,wei,finney,szabo或ether、时间字面量,seconds,minutes,hours,days,weeks,years日期计算并不精确

8. 控制结构,不支持switch和goto,没有非Boolean类型的自动转换

9. throw,可以使用throw来手动抛出一个异常。抛出异常的效果是当前的执行被终止且被撤销(值的改变和帐户余额的变化都会被回退),没有捕捉异常的功能。如果发生运行时异常,或assert失败时,将执行无效操作(指令0xfe)

10. 合约,构造器函数是可选的,仅能有一个构造器不支持重载。函数和状态变量提供了四种可见性。分别是external,public,internal,private。自动为所有的public的状态变量创建访问函数

11. 修饰符 modifer,是一种合约属性(比如用于在函数执行前检查某种前置条件),可被继承,同时还可被派生的合约重写(override)。特殊”_”表示使用修改符的函数体的替换位置。如果同一个函数有多个修改器,他们之间以空格隔开,修饰器会依次检查执行。在修改器中和函数体内的显式的return语句,仅仅跳出当前的修改器和函数体。返回的变量会被赋值,但整个执行逻辑会在前一个修改器后面定义的”_”后继续执行。

12. 回退函数,合约要接收ether,必须实现回退函数。如果调用合约时,没有匹配上任何一个函数就会调用默认的回退函数。当合约收到ether且没有任何其它数据,这个函数也会被执行。回退函数可执行的操作会比常规的花费得多一点,推荐在部署合约到网络前,保证透彻的测试回退函数来保证执行的花费控制在2300 gas以内。

13. 事件Event,会创建EVM log,最多可以有三个参数被设置为indexed。设置为索引后,可以允许通过这个参数来查找日志,甚至可以按特定的值过滤。如果数组(包括string和bytes)类型被标记为索引项,会用它对应的Keccak-256哈希值做为topic。所有未被索引的参数将被做为日志的一部分被保存起来。可以通过函数log0log1log2log3log4直接访问底层的日志组件。

二、关键点

1. 整形 int8 ~ int256 不支持八进制表示,没有浮点型,字面量不保证小数计算精度。注意类型推断编译器是根据第一次变量赋值类型进行推断,所以代码for (var i = 0; i < 2000; i++) {}将是一个无限循环,因为一个uint8的i的将小于2000

2. 地址 address 大小20个字节,160位,所有的合约都会继承地址对象,字面量可以通过balance属性获得一个地址的余额,send方法向某个地址发送货币(货币单位是wei)使用这个方法要检查成功与否(gas不够会执行失败),call方法函数支持传入任意类型的任意参数,并将参数打包成32字节,相互拼接后向合约发送这段数据。支持ABI协议定义的函数选择器。如果第一个参数恰好4个字节,在这种情况下,会被认为根据ABI协议定义的函数器指定的函数签名。所以如果你只是想发送消息体,需要避免第一个参数是4个字节。

3. 数据的存储位置,可选为memorystorage

4. 特殊变量与函数,为了可扩展性的原因,你只能查最近256个块,所有其它的将返回0

  • 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)交易的发送者(完整的调用链)

5. 数学与加密函数,参数是“紧密打包(tightly packed)”的,意思是说参数不会补位,就直接连接在一起的。如果需要补位,需要明确的类型转换。在私链上运行sha256,ripemd160或ecrecover等可能会出现Out-Of-Gas报错。因为它们实现了一种预编译的机制,但合约要在收到第一个消息后才会存在。在你真正使用它们前,先发送1 wei到这些合约上来完成初始化。主链及测试链没有这个问题。

  • keccak256(…) returns (bytes32) 使用以太坊的(Keccak-256)计算HASH值。紧密打包。
  • sha3(…) returns (bytes32)  等同上面
  • sha256(…) returns (bytes32) 使用SHA-256计算HASH值。紧密打包。
  • ripemd160(…) returns (bytes20)  使用RIPEMD-160计算HASH值。紧密打包。
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)  通过签名信息恢复非对称加密算法公匙地址。如果出错会返回0
  • revert() 取消执行,并回撤状态变化。

6. constant可以修饰函数(这类函数将承诺自己不修改区块链上任何状态)与变量(当前支持的仅有值类型和字符串),表明不会1)访问storage;2)区块链数据,如nowthis.balanceblock.number;3)合约执行的中间数据,如msg.gas;4)向外部合约发起调用。(但内置函数允许调用)

7. 所有继承的函数都是virtual的,除非明确指定了合约。继承模型类似python

三、安全建议

竞态:调用外部合约导致被接管控制流,导致DAO崩溃的两个主要错误都是这种错误。

重入:在第一次调用函数完成前该函数被多次重复调用。

跨函数竞态:使用两个共享状态变量的不同的函数来进行类似攻击。

竞态既可以发生在跨函数调用,也可以发生在跨合约调用,任何只是避免重入的解决办法都是不够的。建议首先应该完成所有内部的工作然后再执行外部调用。

  • x.transfer(y)和if (!x.send(y)) throw;是等价的。send是transfer的底层实现,建议尽可能直接使用transfer。
  • someAddress.send()和someAddress.transfer() 能保证可重入。 尽管这些外部智能合约的函数可以被触发执行,但补贴给外部智能合约的2,300 gas,意味着仅仅只够记录一个event到日志中。
  • someAddress.call.value()将会发送指定数量的Ether并且触发对应代码的执行。被调用的外部智能合约代码将享有所有剩余的gas,通过这种方式转账是很容易有可重入漏洞的,非常 不安全

需要注意的是使用send() 或transfer() 进行转账并不能保证该智能合约本身重入安全,它仅仅只保证了这次转账操作时重入安全的。攻击者可以在合约创建之前向合约的地址发送wei。合约不能假设它的初始状态包含的余额为零。

如果你选择使用底层方法,一定要检查返回值来对可能的错误进行处理。

四、实例:

https://github.com/OpenZeppelin/zeppelin-solidity
https://github.com/slockit/smart-contract

https://zhuanlan.zhihu.com/p/29690785

本文是全系列中第7 / 8篇:区块链技术

打赏作者
提交看法

抢沙发

还没有评论,你可以来抢沙发