智能合约

之前做审计的时候都是碰到什么学什么,这样的代价就是基础很差, 要一直搜重复的东西,所以决定好好读一遍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
2
3
import * as symbolName from “filename”;
//两种方式等价
import "filename" as symbolName;

如果存在命名冲突,可以在导入时重命名符号。下面代码就创建了新的全局符号aliassymbol2,引用的symbol1symbol2来自”filename“。

1
import {symbol1 as alias, symbol2} from "filename";

注释

可以使用单行注释( // )和多行注释( /*...*/

1
2
3
4
5
//这是一个单行注释
/*
这是一个
多行注释。
*/

此外,还有一种注释叫做 NatSpec 注释,用三斜线( /// )或双星号块( /** ... */ )来写, 它们应该直接用在函数声明或语句的上方。

合约结构

在 Solidity 中,合约类似于面向对象编程语言中的类。 每个合约中可以包含 状态变量, 函数, 函数修饰器, 事件, 错误, 结构类型 和 枚举类型 的声明,且合约可以从其他合约继承。

还有一些特殊种类的合同,叫做 库合约 和 接口合约。

状态变量

状态变量是指其值被永久地存储在合约存储中的变量。

1
2
3
4
5
6
7
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract SimpleStorage {
uint storedData; // 状态变量
// ...
}

函数(function)

函数是代码的可执行单位。 通常在合约内定义函数,但它们也可以被定义在合约之外。

1
2
3
4
5
6
7
8
9
10
11
12
13
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;

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

// 定义在合约之外的辅助函数
function helper(uint x) pure returns (uint) {
return x * 2;
}

函数修饰器(modifier)

函数修饰器可以被用来以声明的方式修改函数的语义

具有同一个modifier的名字但参数不同是不可能的;但是modifier可以被重载/覆写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;

contract Purchase {
address public seller;

modifier onlySeller() { // 修饰器
require(
msg.sender == seller,
"Only seller can call this."
);
_;
}

function abort() public view onlySeller { // 修饰器的使用
// ...
}
}

事件(event)

事件是能方便地调用以太坊虚拟机日志功能的接口

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;

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

function bid() public payable {
// ...
emit HighestBidIncreased(msg.sender, msg.value); // 触发事件
}
}

错误(error )

error为失败情况定义描述性的名称和数据,可以在回滚(revert等)中使用。与字符串相比,error要便宜的多(消耗的gas少),并允许对额外的数据进行编码。可以使用 NatSpec 格式来向用户描述错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;

/// 没有足够的资金用于转账。要求 `requested`。
/// 但只有 `available` 可用。
error NotEnoughFunds(uint requested, uint available);

contract Token {
mapping(address => uint) balances;
function transfer(address to, uint amount) public {
uint balance = balances[msg.sender];
if (balance < amount)
revert NotEnoughFunds(amount, balance); //回滚 error
balances[msg.sender] -= amount;
balances[to] += amount;
// ...
}
}

结构类型(struct)

结构类型是可以将几个变量分组的自定义类型,和c语言一样的

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

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

枚举类型(enum)

enum可用来创建由一定数量的'常量值'构成的自定义类型

1
2
3
4
5
6
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;

contract Purchase {
enum State { Created, Locked, Inactive } // 枚举
}

类型

值类型

以下类型也称为值类型,因为这些类型的变量将始终按值来传递。 也就是说,当这些变量被用作函数参数或者用在赋值语句中时,总会进行值拷贝。

布尔类型

bool :可能的取值为字面常数值 truefalse

运算符:

  • ! (逻辑非)
  • && (逻辑与, "and" )
  • || (逻辑或, "or" )
  • == (等于)
  • != (不等于)

其中与和或都遵循短路原则

整型

int / uint :分别表示有符号和无符号的不同位数的整型变量。 支持关键字 uint8uint256 (无符号,从 8 位到 256 位)以及 int8int256,以 8 位为步长递增。 uintint 分别是 uint256int256 的别名。

运算符:

  • 比较运算符: <=<==!=>=> (返回布尔值)
  • 位运算符: &|^ (异或), ~ (位取反)
  • 算数运算符: +- , 一元运算 - (仅针对有符号整数), */% (取余) , ** (幂), << (左移位) , >> (右移位)

对于一个整数类型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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.8.1;

contract test{
int256 public a;
int256 public b;

constructor(){
a = type(int256).min;
unchecked{
b = -a;
}
}

function compare() public view returns(bool) {
bool jdg = (a==b);
return jdg;
}

}

image-20230517153400833

由于运算结果的类型总是操作数之一的类型,整数除法的结果总是一个整数。在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相同,但有额外的方法transfersend

    payable addressaddress是可以隐式转换的,但addresspayable address必须通过显式转换:payable(<address>)

    uint160,整数字面量、bytes20和合约类型都可以和address进行显式转换

    只有address和合约类型可以通过payable转换成address payable。对于一个合约类型,只有它可以接收以太币或存在一个payable fallback function才可以完成转换。注意 payable(0)是许可的。

注意:

  1. 如果你需要一个address类型的变量,并计划向其发送以太币,那么请将其类型声明为payable address
  2. addressaddress payable的区别在0.5.0版本中引入的。同样从该版本开始,合约不能隐含地转换为address类型。但如果它们有一个接收或payable fallback function,仍然可以显式地转换为addressaddress payable

如果把一个较大的bytes转换成一个address,例如bytes32,那么address将被截断。例如,b = '0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC':

  • address(uint160(bytes20(b))) = 0x111122223333444455556666777788889999aAaa
  • address(uint160(uint256(b))) = 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc

地址成员

  • balancetransfer

    查询某个地址的余额使用balance,向某个payable address发送以太币(wei为单位)使用transfer:

    1
    2
    3
    address 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, delegatecallstaticcall

    为了与不遵守ABI的合约交互,或者为了更直接地控制编码,我们提供了函数calldelegatecallstaticcall。它们都接受一个字节的内存参数,并返回成功条件(作为一个bool)和返回的数据(字节内存)。函数abi.encode, abi.encodePacked, abi.encodeWithSelectorabi.encodeWithSignature可用于编码结构化数据。

    1
    2
    3
    bytes 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的目的是为了使用在另一个合约中的库代码

    staticcallcall相似,但它会在调用的函数改变状态后revert

    所有这三个函数call, delegatecallstaticcall都是非常低级的函数,只应该作为最后的手段使用,因为它们破坏了Solidity的类型安全。

    gas选项在所有三种方法中都免费,而value选项只在call中免费。

  • codecodehash

    可以查询任何智能合约的部署代码。使用.code来获取EVM字节码,作为字节内存,它可能是空的。使用.codehash获得该代码的Keccak-256哈希值(作为bytes32)。注意,addr.codehash比使用keccak256(addr.code)更便宜。

    所有的合同都可以转换为address类型,所以可以用address(this).balance查询当前合同的余额。

合约类型

每个合约都有自己的类型,它可以通过adress(x)显式地转化成address类型;如果合约中存在receivepayable fallback函数,则能转换成payable address类型;否则需要用payable(address(x))来转换。

注意:在0.5.0版本以前,addresspayable address没有区别。

合约的数据表示与地址类型相同,这种类型也在ABI中使用。

合约不支持任何操作符。

合约的成员包括合约的public函数和任何标记public的状态变量。

对于一个合约c,你可以使用type(c)来访问合同的类型信息。

定长字节数组

bytes1, bytes2, bytes3, ..., bytes32保存一串从1到32的字节。

操作符:

  • 比较: <=, <, ==, !=, >=, > (评估为bool)
  • 比特运算符:&, |, ^ (bit异或), ~ (比特取反)
  • 移位运算符: <<(左移),>>(右移)
  • 索引访问: 如果xbytesI类型的,那么x[k]对于0 <= k < I返回第k个字节(只读)。

成员:

  • .length 返回定长数组的长度(只读)

注意:bytes1[]类型是一个字节数组,但是由于填充规则,它为每个元素浪费了31个字节的空间(除了在存储中)。最好使用bytes类型来代替。

注意:在0.8.0版本之前,byte曾经是bytes1的别名。

变长字节数组

bytesstring

地址字面量

EIP-55.为标准,不通过校验将无法成为地址

有理数和整数字面量

小数点字面量是由.与小数点后的至少一个数字组成。例子包括.11.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;也可以转换成bytesstring

字符字面量只能包含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"

十六进制字面量不能隐式转换成字符字面量

枚举