智能合约
之前做审计的时候都是碰到什么学什么,这样的代价就是基础很差, 要一直搜重复的东西,所以决定好好读一遍Solidity 中文文档 — Solidity中文文档 — 登链社区 (learnblockchain.cn),然后再搞别的,预期这个月以内
[toc]
Solidity源文件结构
源文件中可以包含任意数量的 合约定义 , 源文件引入 , pragma 、 using for 指令和 struct , enum , function , error 以及常量定义。
SPDX版本许可标识
全称为“The Softwaere Package Data Exchange",用来说明其版权许可证
// SPDX-License-Identifier: MIT
版本标识
1 | pragma |
Pragma 是 pragmatic information 的简称,启用编译检查功能,检查版本是否匹配,例如
1 | pragma solidity ^0.5.2; |
表示可被大于等于0.5.2且小于0.6.0的版本编译。
导入其他源文件
solidity支持导入语句来模块化代码
1 | import "filename" |
filename
被称为导入路径
但不建议使用这种形式,它会把导入路径中的全局符号都导入到当前全局作用域,会无法预测的污染当前命名空间,可以使用如下形式
1 | import * as symbolName from “filename”; |
如果存在命名冲突,可以在导入时重命名符号。下面代码就创建了新的全局符号alias
和symbol2
,引用的symbol1
和symbol2
来自”filename“。
1 | import {symbol1 as alias, symbol2} from "filename"; |
注释
可以使用单行注释( //
)和多行注释(
/*...*/
)
1 | //这是一个单行注释 |
此外,还有一种注释叫做 NatSpec 注释,用三斜线( ///
)或双星号块( /** ... */
)来写,
它们应该直接用在函数声明或语句的上方。
合约结构
在 Solidity 中,合约类似于面向对象编程语言中的类。 每个合约中可以包含 状态变量, 函数, 函数修饰器, 事件, 错误, 结构类型 和 枚举类型 的声明,且合约可以从其他合约继承。
还有一些特殊种类的合同,叫做 库合约 和 接口合约。
状态变量
状态变量是指其值被永久地存储在合约存储中的变量。
1 | // SPDX-License-Identifier: GPL-3.0 |
函数(function)
函数是代码的可执行单位。 通常在合约内定义函数,但它们也可以被定义在合约之外。
1 | // SPDX-License-Identifier: GPL-3.0 |
函数修饰器(modifier)
函数修饰器可以被用来以声明的方式修改函数的语义
具有同一个modifier的名字但参数不同是不可能的;但是modifier可以被重载/覆写
1 | // SPDX-License-Identifier: GPL-3.0 |
事件(event)
事件是能方便地调用以太坊虚拟机日志功能的接口
1 | // SPDX-License-Identifier: GPL-3.0 |
错误(error )
error为失败情况定义描述性的名称和数据,可以在回滚(revert等)中使用。与字符串相比,error要便宜的多(消耗的gas少),并允许对额外的数据进行编码。可以使用 NatSpec 格式来向用户描述错误。
1 | // SPDX-License-Identifier: GPL-3.0 |
结构类型(struct)
结构类型是可以将几个变量分组的自定义类型,和c语言一样的
1 | // SPDX-License-Identifier: GPL-3.0 |
枚举类型(enum)
enum可用来创建由一定数量的'常量值'构成的自定义类型
1 | // SPDX-License-Identifier: GPL-3.0 |
类型
值类型
以下类型也称为值类型,因为这些类型的变量将始终按值来传递。 也就是说,当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。
布尔类型
bool
:可能的取值为字面常数值 true
和
false
运算符:
!
(逻辑非)&&
(逻辑与, "and" )||
(逻辑或, "or" )==
(等于)!=
(不等于)
其中与和或都遵循短路原则
整型
int
/ uint
:分别表示有符号和无符号的不同位数的整型变量。 支持关键字
uint8
到 uint256
(无符号,从 8 位到 256
位)以及 int8
到 int256
,以 8
位为步长递增。 uint
和 int
分别是
uint256
和 int256
的别名。
运算符:
- 比较运算符:
<=
,<
,==
,!=
,>=
,>
(返回布尔值) - 位运算符:
&
,|
,^
(异或),~
(位取反) - 算数运算符:
+
,-
, 一元运算-
(仅针对有符号整数),*
,/
,%
(取余) ,**
(幂),<<
(左移位) ,>>
(右移位)
对于一个整数类型X,你可以使用type(X).min和type(X).max来访问该类型可表示的最小和最大值。
位操作
位操作是在数字的补码进行的。这意味着,例如~int256(0) == int256(-1)
移位
移位操作的结果将截断成左操作数的类型;右操作数必须是无符号类型,否则会产生编译错误。
x << y
等价于x * 2**y
.x >> y
等价于x / 2**y
,向负无穷取整在
0.5.0
版本之前,负数x的右移x>>y
相当于数学表达式x/2**y
向零舍入,即右移使用向上取整(向零)而不是向下取整(向负无穷)。
ps:对移位操作从不进行溢出检查,就像对算术操作那样。相反,结果总是被截断的。
加减乘
加法、减法和乘法具有通常的语义,在溢出和下溢方面有两种不同的模式:
默认情况下,所有的算术都会被检查是否有下溢,但这可以通过unchecked块来禁用,从而导致包装算术。更多的细节可以在该部分找到。
表达式-x相当于(T(0) - x),其中T是x的类型。如果x是负的,-x的值可以是正的。还有一个注意事项也是由补码引起的:
如果你有int x = type(int).min;,那么-x就不符合正的范围。这意味着未经检查的{ assert(-x == x); }是有效的,而表达式-x在检查模式下使用时将导致一个失败的断言。
1 | pragma solidity ^0.8.1; |
除
由于运算结果的类型总是操作数之一的类型,整数除法的结果总是一个整数。在Solidity中,除法是向零进位的。这意味着int256(-5) / int256(2) == int256(-2)
。
请注意,与此相反,在字面量(literals)上除法的结果是任意精度的小数值。
除以0为导致报错,并且不能使用unchecked{}
来禁用。
type(int).min / (-1)
这个表达式是除法导致上溢的唯一情况
模
模操作a % n
产生操作数a
除以操作数n
后的余数r
,其中q = int(a / n),r = a - (n * q)
。这意味着模运算的结果与它的左边操作数(或零)相同,a % n == -(-a % n)
对负数a
成立:
int256(5) % int256(2) == int256(1)
int256(5) % int256(-2) == int256(1)
int256(-5) % int256(2) == int256(-1)
int256(-5) % int256(-2) == int256(-1)
模0会导致报错,并且不能使用unchecked{}
来禁用。
幂
建议用x*x*x
来代替x**3
来减少gas消耗
EVM定义0**0=1
地址
地址有两种:
address
: 保存一个20字节的值(以太坊地址的大小)。address payable
: 与address
相同,但有额外的方法transfer
和send
。payable address
到address
是可以隐式转换的,但address
到payable address
必须通过显式转换:payable(<address>)
uint160
,整数字面量、bytes20
和合约类型都可以和address
进行显式转换只有
address
和合约类型可以通过payable
转换成address payable
。对于一个合约类型,只有它可以接收以太币或存在一个payable fallback function
才可以完成转换。注意payable(0)
是许可的。
注意:
- 如果你需要一个
address
类型的变量,并计划向其发送以太币,那么请将其类型声明为payable address
address
和address payable
的区别在0.5.0版本中引入的。同样从该版本开始,合约不能隐含地转换为address
类型。但如果它们有一个接收或payable fallback function
,仍然可以显式地转换为address
或address payable
。
如果把一个较大的bytes转换成一个address
,例如bytes32
,那么address
将被截断。例如,b = '0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC'
:
address(uint160(bytes20(b))) = 0x111122223333444455556666777788889999aAaa
address(uint160(uint256(b))) = 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc
地址成员
balance
和transfer
查询某个地址的余额使用balance,向某个payable address发送以太币(wei为单位)使用transfer:
1
2
3address payable x = payable(0x123);
address myAddress = address(this);
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);如果x是一个合约地址,它的代码(实际上是接受以太坊的函数,或
Fallback
函数)会连同transfer
一起执行。这是EVM的一个特性,无法被阻止。如果这段执行将gas消耗完或其他原因失败,transfer
就会被回退send
send
是低级别的transfer
,如果执行失败则会返回false
而不是回退使用
send
有一些危险: 如果调用堆栈深度为1024,转账就会失败(这可以由调用者强制执行),如果接收者没有gas
了,也会失败。因此,为了安全地进行以太币转账,一定要检查send
的返回值、使用transfer
,甚至更好的是:使用收款人取款的模式call
,delegatecall
和staticcall
为了与不遵守ABI的合约交互,或者为了更直接地控制编码,我们提供了函数
call
、delegatecall
和staticcall
。它们都接受一个字节的内存参数,并返回成功条件(作为一个bool
)和返回的数据(字节内存)。函数abi.encode
,abi.encodePacked
,abi.encodeWithSelector
和abi.encodeWithSignature
可用于编码结构化数据。1
2
3bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);可以调节gas消耗量:
1
address(nameReg).call{gas: 1000000}(abi.encodeWithSignature("register(string)", "MyName"));
以太币也可以调节:
1
address(nameReg).call{value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));
甚至是组合:
1
address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));
同样的,
delegatecall
也可以被这样使用,但不同的点在于:执行环境为当前合约的环境,目标合约地址只是被调用,delegatecall
的目的是为了使用在另一个合约中的库代码staticcall
与call
相似,但它会在调用的函数改变状态后revert
所有这三个函数
call
,delegatecall
和staticcall
都是非常低级的函数,只应该作为最后的手段使用,因为它们破坏了Solidity的类型安全。gas
选项在所有三种方法中都免费,而value
选项只在call
中免费。code
和codehash
可以查询任何智能合约的部署代码。使用
.code
来获取EVM
字节码,作为字节内存,它可能是空的。使用.codehash
获得该代码的Keccak-256
哈希值(作为bytes32
)。注意,addr.codehash
比使用keccak256(addr.code)
更便宜。所有的合同都可以转换为
address
类型,所以可以用address(this).balance
查询当前合同的余额。
合约类型
每个合约都有自己的类型,它可以通过adress(x)
显式地转化成address
类型;如果合约中存在receive
或payable fallback
函数,则能转换成payable address
类型;否则需要用payable(address(x))
来转换。
注意:在0.5.0
版本以前,address
和payable address
没有区别。
合约的数据表示与地址类型相同,这种类型也在ABI
中使用。
合约不支持任何操作符。
合约的成员包括合约的public
函数和任何标记public
的状态变量。
对于一个合约c
,你可以使用type(c)
来访问合同的类型信息。
定长字节数组
bytes1, bytes2, bytes3, ..., bytes32
保存一串从1到32的字节。
操作符:
- 比较:
<=
,<
,==
,!=
,>=
,>
(评估为bool) - 比特运算符:
&
,|
,^
(bit异或),~
(比特取反) - 移位运算符:
<<
(左移),>>
(右移) - 索引访问:
如果
x
是bytesI
类型的,那么x[k]
对于0 <= k < I
返回第k
个字节(只读)。
成员:
.length
返回定长数组的长度(只读)
注意:bytes1[]
类型是一个字节数组,但是由于填充规则,它为每个元素浪费了31个字节的空间(除了在存储中)。最好使用bytes
类型来代替。
注意:在0.8.0
版本之前,byte
曾经是bytes1
的别名。
变长字节数组
bytes
和string
地址字面量
以EIP-55.为标准,不通过校验将无法成为地址
有理数和整数字面量
小数点字面量是由.
与小数点后的至少一个数字组成。例子包括.1
和1.3
(但不能是1.
)。也支持科学计数法,字面量MeE等价于M*10**E
,指数部分必须是整数,尾数可以是小数。
下划线可用来提高代码可读性,没有其他任何意义。例如,十进制123_000,十六进制0x2eff_abde,科学十进制符号1_2e345_678都是有效的。下划线只允许在两个数字之间,并且只允许一个连续的下划线。
数字字面表达式保留了任意精度,直到被转换成其他非字面类型(即通过与数字字面表达式以外的其他东西一起使用(如布尔字面)或显式转换)。在数字字面表达式中,计算不会溢出,除法不会截断。例如,2**800+1-2**800
在字面量中是允许的。.5*8
的结果是整型的4
。
只要操作数是整数,任何可以应用于整数的操作符也可以应用于数字字面表达式。如果两者中的任何一个是小数,则不允许进行位操作,如果指数是小数,则不允许进行指数化(因为这可能导致非有理数)。
以字面数为左(或基数)操作数,以整数类型为右(指数)操作数的移位和指数化,总是在uint256
(对于非负的字面数字)或int256
(对于负的字面数字)类型中进行,与右(指数)操作数的类型无关。
在0.4.0
之前,整数的字面量会被截断即5/2=2
,之后是2.5
字符字面量和类型
字符字面量可以分开写,例如“foo""bar"
等价于”foobar"
;并且"foo"
只占用3个字节而不是4个字节(对比C语言的字符串以"\0"
结尾);它们可以隐式地转换成bytes1
,
…,
bytes32
;也可以转换成bytes
和string
。
字符字面量只能包含ASCII可打印字符,也就是0x20到0x7E;此外,也包括以下转义符:
\<newline>
(转义实际换行)\\
(反斜杠)\'
(单引号)\"
(双引号)\n
(换行符)\r
(回车)\t
(标签 tab)\xNN
(十六进制转义,表示一个十六进制的值,)\uNNNN
(unicode 转义,转换成UTF-8的序列)
注意:直到0.8.0才推出:\b
(退格)、\f
(换页)、\v
(垂直标签);但如果实在需要用到,可以通过十六进制转义插入,即分别为\x08
、\x0c
和\x0b
。
任何unicode行终结符(除了LF,VF,FF,CR,NEL,LS,PS)都可以当成字符串字面常量的终止符。
只有当字符串字面常量前没有\
时,换行符才能终止字符串字面常量。
Unicode字面量
Unicode字面量—以unicode
开头—可以包含所有有效的UTF-8字符,例如:
1 | string memory a = unicode"Hello 😃"; |
十六进制字面量
以"hex"开头,紧跟单引号或双引号字符串: hex"001122FF"
,
hex'0011_22_FF'
,可以使用下划线作为字符的分隔符。
用空格分隔的十六进制字面量将表达成一个拼接起来的十六进制字面量:hex"00112233" hex"44556677"
等价于 hex"0011223344556677"
十六进制字面量不能隐式转换成字符字面量