0%

《Solidity 智能合约教程》Solidity 基础

Solidity 基础讲解起来难免有一些枯燥乏味而且记不住

所以我选择理论结合案例的方式来进行讲解

下面是我备课时做的一个完整案例

https://github.com/pefish/lesson-solidity/tree/master/example/learn

大家可以 clone 下来,对照着学习,看不懂不要紧,接下来就是教你如何看懂并学会如何写更多的智能合约

SPDX-License-Identifier 注释

// SPDX-License-Identifier: MIT

SPDX-License-Identifier 注释被用于指定开源协议,也就是声明自己所写代码的版权,类似于 github repo 中的 License 文件中的版权声明

https://spdx.org/licenses/ 这里可以看到所有类型的版权声明,这些版权声明中不同程度的规定了代码的使用权

当然,如果你不想指定一个开源协议,或者你的代码根本就不开源,你可以使用 UNLICENSED,你也可以不注释,不写这一行,这只是一个约定写法

pragma 关键字

pragma 关键字是用来做一些版本的声明

pragma solidity

pragma solidity 用来指定当前文件中的代码应该被哪些版本的编译器编译

不同版本的编译器,语法特性很可能不一样,所以也可以说是用来规定当前文件中可以使用哪些编译器版本下的语法特性

例如 pragma solidity ^0.5.2; 规定了当前文件只能被 0.5.2 - 0.5.* 版本的编译器所编译

不在这个区间的编译器进行编译时会导致失败

pragma abicoder

pragma abicoder 用来指定 abicoder 的版本,更新的版本的 abicoder 往往支持更多的类型,编程会更加的方便

0.8.0 之前的编译器中,可以通过 pragma experimental ABIEncoderV2 来使用更多的数据类型

而在 0.8.0 的编译器中,pragma abicoder v2 已经是默认声明

pragma experimental

pragma experimental 用于指定你要使用哪些还没有被默认启用的新特性

比如上面说的 0.8.0 之前的 pragma experimental ABIEncoderV2 声明

import 关键字

import 关键字跟 JS 的 ES6 语法差不多,用法如下

import * as symbolName from "./filename"; 将文件中的所有顶级元素导入到 symbolName 中

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

尽量不要使用 import * as A from "../test.sol" 这种 ../ 多级相对目录

要么使用 ./ 要么使用 remapping 绝对路径

remapping 绝对路径

就是一个前缀被别名替换的绝对路径

例如 import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol";

你将 github.com/ethereum/dapp-bin 中的代码下载到目录 /local/test 下的话

使用 solc github.com/ethereum/dapp-bin/=/usr/test/ source.sol 命令编译就不会有任何问题

顺便一提:

  1. 在 remix IDE 中,github 的网址会被自动 remapping,你可以直接导入 github 上的任何代码,remix 负责替你下载源码,替你 remapping
  2. 在 truffle 中可以直接像 js 中使用 npm 包一样。import "@pefish/solidity/test.sol"

变量

声明变量的格式如下

<类型> <修饰(可多个)> <变量名> = <初始值>;

例如

1
uint256 private words = 1;

类型

Solidity 是一门静态语言,和其他静态语言一样需要进行类型声明

Solidity 没有 undefined、null、nil,每个类型都有自己的零值(例如 uint256 类型变量的零值就是 0)

bool

int / uint

address

占用 20 字节。分为 address 和 address payable 两种,后者带有 transfer 和 send 两个函数属性,也就是说前者不可以收发 ETH,后者却可以。

后者可以隐含转换成前者,而前者要通过 payable(

) 强制转换为后者

address 类型具有下面属性

  • .balance (uint256) // 获取地址余额
  • .code (bytes memory) // code at the Address (can be empty)
  • .codehash (bytes32) // the codehash of the Address
  • .transfer(uint256 amount) // 向地址发送 ETH,转发 2300 gas
  • .send(uint256 amount) returns (bool) // 向地址发送 ETH,转发 2300 gas。与上面的区别是,转账失败了这个函数不会抛出异常而是返回 false,推荐使用 transfer
  • .call(bytes memory) returns (bool, bytes memory) // 调用合约,转发所有可用的 gas。跟 send 一样,失败不会抛异常,只会返回 false
1
2
3
4
5
bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);

address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));
  • .delegatecall(bytes memory) returns (bool, bytes memory) // call 就相当于普通的 api 接口调用,delegatecall 则是借用目标合约的目标函数的代码(注意只是代码,不会利用存储),上下文(任何全局或局部变量)还是在本合约中,可以利用这个特性实现可升级合约(后面会做实例讲解)。delegatecall 调用时,代理合约的 slot 位置完全对应 delegatecall 合约的 slot 位置,与变量名无关
  • .staticcall(bytes memory) returns (bool, bytes memory) // 调用合约的只读函数

array

数组分为静态数组和动态数组

动态数组在内存中的布局是:第一个 32 字节的内容表示动态数组的长度,后面才是动态数组的内容(类似于 golang 中的 slice 有一个长度的字段一样)

memory 生命周期的 array 只能是静态数组,通过 new 来初始化,而且必须指定大小,而且 push 函数不可用,不能自动扩容

1
2
uint[] memory a = new uint[](7);
bytes memory b = new bytes(7);

但是 storage 生命周期的 array 可以是动态数组,通过 push 自动扩容

例如下面的代码会在运行时报错,而且在编译时无法察觉,这类问题最影响开发速度

1
2
3
address[] memory sellPath;
// address[] memory sellPath = new address[](1); // 改成这样才正确
sellPath[0] = address(mdxContract);

array 类型具有下面属性

  • length // 返回数组的当前长度
  • push() // 放入一个零值
  • push(x) // 放入一个值
  • pop // 移除末尾的值

当前只有 calldata 生命周期内的 array 支持数组的切片语法 x[start:end]

bytes

是个特别的动态数组,等于 byte[]。但是最好使用 bytes ,更加的节省内存,[]byte 会在每个元素之间加上 31 个占位字节,非常浪费空间

string

string 可以隐式按照 utf8 编码转化为 bytes,也可以说是个特别的数组

如果要使用 unicode 编码的字符串,可以这样 string memory a = unicode"Hello 😃";

struct

1
2
3
4
5
6
struct TestStruct {
uint weight;
bool voted;
address delegate;
uint vote;
}

enum

1
enum TestEnum { Created, Locked, Inactive }

function

函数的格式如下

function <函数名> (<参数类型> <参数修饰> <参数名>, <...>) <internal|external> [pure|view|payable] [修改器] [returns (<返回值类型>, <...>)] { <函数体> }

函数也可以声明在顶层

external 或者 public 的函数具有下面属性:

  1. .address // 返回函数所在的合约的地址
  2. .selector // 返回 abi 函数选择器
  • view 函数

不会修改链上数据的函数应该声明为 view 函数

以下几种行为都属于修改了链上数据

  1. 更改了状态变量
  2. emit 了事件
  3. 创建了其他合约
  4. 使用了 selfdestruct 销毁自己
  5. 转账了 eth
  6. 调用了非 view 或者非 pure 的函数
  7. 使用了低级汇编代码
  • pure 函数

不会修改链上数据而且也不会读链上数据的函数应该声明为 pure 函数

以下几种行为都属于读链上数据

  1. 读状态变量
  2. 使用了 address(this).balance 或者
    .balance
  3. 使用了 block, tx, msg 的任何属性( msg.sig 和 msg.data 除外)
  4. 调用了非 pure 函数
  5. 使用了低级汇编
  • payable 函数

允许外部调用这个函数时发送以太坊,如果调用没有 payable 修饰的函数时还发送了 eth,则会 revert

  • receive 函数

receive 函数用来作为合约地址接收到以太坊的回调函数,最多只能有一个,用法如下:

receive() external payable [修改器] { ... }

注意:receive 函数是不带 function 关键字的

因为 send 和 transfer 的以太坊转账只会转发 2300 个gas,所以 receive 函数只有 2300 个 gas 可用,做不了什么事

下列操作都会超过 2300 个 gas

  1. 写存储
  2. 创建合约
  3. 调用消耗大量 gas 的外部函数
  4. 发送 eth

但如果是 address.call.value()() 进行 ether 转币,则会转发所有 gas,能做的事情很多

  • fallback 函数

当调用的函数在合约中不存在,则会调用 fallback 函数,包括 send 和 transfer(当存在 receive 函数的时候,不会调用 fallback 函数了。也同样存在 2300 gas 的情况)

用法如下:

fallback ([bytes calldata _input]) external [payable] [returns (bytes memory _output)]

注意:fallback 函数也是不带 function 关键字的

modifier

函数修改器类似于 Java、JS ES6 等语言里面的修饰器

modifier <修改器名称> (<参数类型> <参数修饰> <参数名>, <...>) { <函数体> }

event

智能合约中,修改了某些状态后,可以通过发出事件记录下修改这次行为。可以理解为打日志,已被后面查询

格式如下

event <事件名称> (<参数类型> <参数修饰> <参数名>, <...>)

其中有一个参数修饰是 indexed ,用来表示这个参数用作索引,查询日志时就可以根据这个索引进行过滤

contract

contract 相当于其他语言中的类,可以通过 new 关键字实例化

contract 继承了 address 类型的所有属性

  • this:指代当前合约,可以强转成 address。使用 this.*() 调用当前合约中的方法时,相当于外部调用,被调用函数上的修改器同样会执行(看案例)

  • selfdestruct(address payable recipient):销毁当前合约,并且将合约中的以太坊发送给指定地址,然后中止执行。注意:如果接受地址为合约,将不会执行其 receive 函数

  • 以太坊智能合约中,天然具有”分布式事务”能力,不管合约之间如何调用,只要交易失败了,任何合约中的数据都会被回滚(看案例)

创建合约时,合约地址是由 创建者地址 以及 全局的已创建合约的计数器 计算得到

但是如果创建时指定了 salt 选项,则合约地址的生成机制变得不一样。是由 创建者地址、给定的 salt 值、目标合约的创建字节码以及合约的构造函数的参数 计算得到

这种 salt 方式创建合约的好处是,你可以根据 salt 和合约参数推断出合约的地址,一般可以将 salt 设置为 keccak256(abi.encodePacked(arg0, arg1)) ,这样的话,合约的参数决定了 salt,那么合约的参数就直接决定了合约的地址

比如去中心化交易所中,每一个交易对都是一个独立合约,当你想要调用 BTC/USDT 合约,你可以使用 BTC、USDT 两个构造函数参数推断出合约地址,然后调用他即可,而不需要在合约中维护一个 交易对/合约地址 的 map

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
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract D {
uint public x;
constructor(uint a) {
x = a;
}
}

contract C {
function createDSalted(bytes32 salt, uint arg) public {
address predictedAddress = address(
uint160(
uint(
keccak256(
abi.encodePacked(
bytes1(0xff), // 填充
address(this), // 创建者地址
salt, // salt
keccak256(
abi.encodePacked(
type(D).creationCode, // 目标合约的创建字节码
arg // 目标合约的参数
)
)
)
)
)
)
);

D d = new D{salt: salt}(arg);
require(address(d) == predictedAddress);
}
}

library

与 contract 类似,但 library 中的函数都是静态函数,都是以 DELEGATECALL 的方式被其他合约使用,常用于工具类

  • Using For 语法

对于 library,可以使用 Using For 语法,using <library> for <type/*>;,意思是将库的所有函数设置成指定类型的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct Data { mapping(uint => bool) flags; }

library Set {
function insert(Data storage self, uint value)
internal
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
}


contract C {
using Set for Data; // this is the crucial change
Data knownValues;

function register(uint value) public {
require(knownValues.insert(value));
}
}

如果 library 中有任何一个 external、public 或者非 view、pure 函数,则要引用这个 library ,必须先部署这个 library ,否则不用

因为 external、public 意味着这个 library 具有存储,当然得部署以获得存储

引用类型

所有的引用类型都应该声明其生命周期,storage、memory 或者 calldata

特别的,contract 内的成员变量默认就是 storage ,无需声明

  • storage: 生命周期与合约的生命周期一致

  • memory: 生命周期被限制在函数调用内

  • calldata: 生命周期被限制在函数参数中。可以避免复制,且可以确保内容不被修改(calldata 修饰的引用类型变量是只读的)

看下面官方给的一个例子,解释了什么时候值传递,什么时候指针传递

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
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;

contract C {
// The data location of x is storage.
// This is the only place where the
// data location can be omitted.
uint[] x;

// The data location of memoryArray is memory.
function f(uint[] memory memoryArray) public {
x = memoryArray; // works, copies the whole array to storage
uint[] storage y = x; // works, assigns a pointer, data location of y is storage
y[7]; // fine, returns the 8th element
y.pop(); // fine, modifies x through y
delete x; // fine, clears the array, also modifies y
// The following does not work; it would need to create a new temporary /
// unnamed array in storage, but storage is "statically" allocated:
// y = memoryArray;
// This does not work either, since it would "reset" the pointer, but there
// is no sensible location it could point to.
// delete y;
g(x); // calls g, handing over a reference to x
h(x); // calls h and creates an independent, temporary copy in memory
}

function g(uint[] storage) internal pure {}
function h(uint[] memory) public pure {}
}

map

1
mapping(address => uint256) public balances;

map 永远是 storage 类型的,就是说 map 中的内容无法被删除

假如 a 是 struct ,则 delete a 就是将 struct 中除了 map 以外的所有元素置为零值,map 无法被清零

假如 a 是 map 数组,delete a 也无法删除数组中 map 里面的内容,只是数组的长度变成了0,如果数组的长度恢复了,同样可以访问原来 map 中的内容

如果想要遍历 map ,可以使用 https://github.com/pefish/solidity-lib/blob/master/contracts/util/IterableMapping.sol

mapping 是没有 length 属性的,想要获取 map 的长度,也可以使用上面的库

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
// SPDX-License-Identifier: MIT

pragma solidity >=0.8.0;

import { IterableMapping } from "../library/IterableMapping.sol";

contract IterableMappingMock {
IterableMapping.Itmap private data;

function get (uint256 key) public view returns (uint256) {
return data.data[key].value;
}

function insert(uint256 k, uint256 v) public returns (uint256 size) {
IterableMapping.insert(data, k, v);
return data.size;
}

function sum() public view returns (uint256 s) {
for (
uint256 i = IterableMapping.iterate_start(data);
IterableMapping.iterate_valid(data, i);
i = IterableMapping.iterate_next(data, i)
) {
(, uint256 value) = IterableMapping.iterate_get(data, i);
s += value;
}
}
}

tuple

tuple 类型其实就是 struct 类型

tuple 元组严格来讲并不是 solidity 中的类型,但是可以用在返回值中

1
2
3
4
5
6
7
8
9
10
11
12
13
contract C {
uint index;

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

function g() public {
(uint x, , uint y) = f();
(x, y) = (y, x);
(index, , ) = f(); // Sets the index to 7
}
}

tuple 类型和 struct 类型一样,内部的类型都是对应 golang 当中的 struct

interface

1
2
3
interface <接口名> {
function getData(address token) external returns (uint value);
}

delete 关键字

delete 可以操作多种类型的数据,作用就是将其设为零值

  1. 假如 a 是整型数据,则 delete a 就相当于 a = 0
  2. 假如 a 是静态数组,则 delete a 就是将数组内元素全部置为零值。delete a[x] 是将目标元素置为零值
  3. 假如 a 是动态数组,则 delete a 就是将数组清空,数组长度变成0。delete a[x] 是将目标元素置为零值
  4. 假如 a 是 map ,则 delete a[x] 就是删除元素
  5. 假如 a 是 struct ,则 delete a 就是将 struct 中除了 map 以外的所有元素置为零值
  6. 对于 uint[] storage y = x,y 引用了 x,则 delete y 不会影响 x,而 delete x 可以影响 y

类型转换

整型中,大类型向小类型强制转换时,是从低位开始截取,反过来则是从高位开始填充

1
2
3
4
5
uint32 a = 0x12345678;
uint16 b = uint16(a); // 0x5678

uint16 a = 0x1234;
uint32 b = uint32(a); // 0x00001234

而在字节数组中,大类型向小类型强制转换时,是从高位开始截取,反过来则是从低位开始填充

1
2
3
4
5
bytes2 a = 0x1234;
bytes1 b = bytes1(a); // 0x12

bytes2 a = 0x1234;
bytes4 b = bytes4(a); // 0x12340000

其实很好理解,对于数组来讲,大数组变小数组,自然应该抛弃数组尾部的元素,因为这样更简单更容易实现,小数组变大数组一样的,尾部填充就可以

而对于整型数据,按上面这样做的话,uint16 转化为 uint32 ,数值都变了,很明显不能这样做

单位

1
2
3
assert(1 wei == 1);  // 后面没有声明单位的就是 wei
assert(1 gwei == 1e9);
assert(1 ether == 1e18);

预定义变量以及函数

预定义变量以及函数是指智能虚拟机中已经定义好了的一些变量和方法,智能合约中直接使用即可

  • blockhash(uint blockNumber) returns (bytes32): 根据区块号获取区块hash(只能查询最近256个区块)

  • block.chainid (uint): 链id

  • block.coinbase (address payable): 当前块的矿工地址

  • block.difficulty (uint): 当前块的出块难度

  • block.gaslimit (uint): 当前块的 gaslimit

  • block.number (uint): 当前块的区块号

  • block.timestamp (uint): 当前块的时间戳,单位秒

  • gasleft() returns (uint256): 获取剩余的gas数量

  • msg.data (bytes calldata): 完整的调用数据

  • msg.sender (address payable): 当前调用的发送者

  • msg.sig (bytes4): 调用数据的前四个字节,也就是调用函数的标识

  • msg.value (uint): 发送的以太坊数量

  • tx.gasprice (uint): 当前交易的 gasPrice

  • tx.origin (address payable): 交易的发送者

  • abi.decode(bytes memory encodedData, (…)) returns (…): 解码不带前面四个字节的数据

  • abi.encode(…) returns (bytes memory): 编码成不带前面四个字节的数据

  • abi.encodePacked(…) returns (bytes memory): 将参数按照另一种规则编码成不带前面四个字节的数据,函数调用没有采用这种方式,上面的 encode 函数才是首选

  • abi.encodeWithSelector(bytes4 selector, …) returns (bytes memory): 编码成完整的带有前面四个字节的数据

  • abi.encodeWithSignature(string memory signature, …) returns (bytes memory): 等于 abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)

错误处理

  • assert(bool condition)
  • require(bool condition)
  • require(bool condition, string memory message)
  • revert()
  • revert(string memory reason)

上面的函数都是抛出异常并回滚状态,最常用的是 require(bool condition, string memory message) ,提供错误信息,利于排错

  • try/catch:可以用来捕获上面函数抛出的异常
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.6.0 <0.9.0;

interface DataFeed { function getData(address token) external returns (uint value); }

contract FeedConsumer {
DataFeed feed;
uint errorCount;
function rate(address token) public returns (uint value, bool success) {
// Permanently disable the mechanism if there are
// more than 10 errors.
require(errorCount < 10);
try feed.getData(token) returns (uint v) {
return (v, true);
} catch Error(string memory /*reason*/) {
// This is executed in case
// revert was called inside getData
// and a reason string was provided.
errorCount++;
return (0, false);
} catch (bytes memory /*lowLevelData*/) {
// This is executed in case revert() was used.
errorCount++;
return (0, false);
}
}
}

一些算法函数

  • addmod(uint x, uint y, uint k) returns (uint): 计算 (x + y) % k 的值
  • mulmod(uint x, uint y, uint k) returns (uint): 计算 (x * y) % k 的值
  • keccak256(bytes memory) returns (bytes32): 计算 hash
  • sha256(bytes memory) returns (bytes32):计算 hash
  • ripemd160(bytes memory) returns (bytes20):计算 hash
  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address):根据签名结果以及 v、r、s 3个参数得到签名者地址

查询类型信息

type(X) 可用于查询类型的信息,X 可以是 contract 、整型、interface,type(X) 的返回值具有以下属性

1
2
3
4
5
6
7
8
9
contract Test {
function test () public pure returns (string memory) {
return type(Test).name;
}

function test1 () public pure returns (uint256) {
return type(uint256).min;
}
}
  • type(C).name:合约的名称。合约类型才有的属性
  • type(C).creationCode:合约的创建字节码。合约类型才有的属性。不能查看自身合约或者子合约
  • type(C).runtimeCode:合约的运行时字节码。合约类型才有的属性
  • type(I).interfaceId:接口的4字节标识。接口类型才有的属性
  • type(T).min:整型类型的最小值。整型类型才有的属性
  • type(T).max:整型类型的最大值。整型类型才有的属性

unchecked 关键字

从 0.8.0 版本的 solidity 开始,任何计算溢出都会被 revert,再也不需要使用 SafeMath 这类库

但是如果你想允许溢出,则可以使用 unchecked 关键字

1
2
3
4
5
6
7
8
9
10
contract C {
function f(uint a, uint b) pure public returns (uint) {
// This addition will wrap on underflow.
unchecked { return a - b; }
}
function g(uint a, uint b) pure public returns (uint) {
// This addition will revert on underflow.
return a - b;
}
}

f(2, 3) 会发生溢出而返回 2**256-1,但是 g(2, 3) 会 revert

*注意:类型的隐式转换并不是溢出,例如 uint256 a = uint256(10) / uint256(3) 中 float 隐式转换成了 uint256 ,结果就是 3 *

可见性

可见性是指变量或者函数是否可以被外部使用或者调用

  • public

修饰的变量以及函数能被其他合约使用,而且也能被其他合约通过交易调用

public 修饰的变量自带 getter 函数,可以被外部查询到

如果函数既是公开的,又要被本合约或继承者其他函数调用,则应当声明为 public

  • external

只能被其他合约通过交易调用,不能修饰变量

通过交易被调用时,由于 public 修饰的函数总是会先将参数 copy 一份到内存,而 external 可以直接使用参数,因此当参数占用空间大时,public 会比 external 更加消耗 gas

只要函数是公开的且不被本合约或继承者其他函数调用的,都应当声明为 external ,而不是 public

  • internal

修饰的变量以及函数能被自己以及继承者使用

如果函数既要被本合约也要被继承者调用,则使用 internal

  • private

修饰的变量以及函数只能自己使用

注意:private 修饰的变量并不意味着数据私密,链上的数据全部都是公开的

如果函数只是被本合约调用,使用 private

常量

下面两个关键词都可以修饰一个不可更改的元素(也就是常量),但有初始化方面的区别

  • constant

必须在编译期初始化

也可以在文件顶级使用

  • immutable

可以在构造函数里初始化

低级汇编

低级汇编用得很少,平时写代码也不要想着去用它,能不用就不用。不仅复杂而且安全性不好

想了解的可以看这里 https://docs.soliditylang.org/en/v0.8.0/assembly.html,以及这里 https://docs.soliditylang.org/en/v0.8.0/yul.html#yul

assembly 块中访问外部变量

对于值类型的本地变量,可以直接使用变量名访问

对于指针类型的本地变量(memory、calldata),可以直接使用变量名访问,但访问到的是变量的地址

对于 storage 类型的本地变量或者状态变量,他们由 slot(变量值所处的 slot 位置) 和 offset( 值在 slot 中开始位置的字节偏移) 组成,x_slot 访问 slot ,x_offset 访问偏移

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

contract C {
uint b;
function f(uint x) public view returns (uint r) {
assembly {
// We ignore the storage slot offset, we know it is zero
// in this special case.
r := mul(x, sload(b_slot))
}
}
}

汇编指令

第二列的 - 表示这个指令不会使任何元素入栈,* 表示所有元素都会入栈。第三列表示自从 Frontier、Homestead、Byzantium 或者 Constantinople 就已经存在

Instruction - - Explanation
stop - F stop execution, identical to return(0,0)
add(x, y) F x + y
sub(x, y) F x - y
mul(x, y) F x * y
div(x, y) F x / y
sdiv(x, y) F x / y, for signed numbers in two’s complement
mod(x, y) F x % y
smod(x, y) F x % y, for signed numbers in two’s complement
exp(x, y) F x to the power of y
not(x) F ~x, every bit of x is negated
lt(x, y) F 1 if x < y, 0 otherwise
gt(x, y) F 1 if x > y, 0 otherwise
slt(x, y) F 1 if x < y, 0 otherwise, for signed numbers in two’s complement
sgt(x, y) F 1 if x > y, 0 otherwise, for signed numbers in two’s complement
eq(x, y) F 1 if x == y, 0 otherwise
iszero(x) F 1 if x == 0, 0 otherwise
and(x, y) F bitwise and of x and y
or(x, y) F bitwise or of x and y
xor(x, y) F bitwise xor of x and y
byte(n, x) F nth byte of x, where the most significant byte is the 0th byte
shl(x, y) C logical shift left y by x bits
shr(x, y) C logical shift right y by x bits
sar(x, y) C arithmetic shift right y by x bits
addmod(x, y, m) F (x + y) % m with arbitrary precision arithmetic
mulmod(x, y, m) F (x * y) % m with arbitrary precision arithmetic
signextend(i, x) F sign extend from (i*8+7)th bit counting from least significant
keccak256(p, n) F keccak(mem[p…(p+n)))
jump(label) - F jump to label / code position
jumpi(label, cond) - F jump to label if cond is nonzero
pc F current position in code
pop(x) - F remove the element pushed by x
dup1 … dup16 F copy nth stack slot to the top (counting from top)
swap1 … swap16 * F swap topmost and nth stack slot below it
mload(p) F mem[p…(p+32))
mstore(p, v) - F mem[p…(p+32)) := v
mstore8(p, v) - F mem[p] := v & 0xff (only modifies a single byte)
sload(p) F storage[p]
sstore(p, v) - F storage[p] := v
msize F size of memory, i.e. largest accessed memory index
gas F gas still available to execution
address F address of the current contract / execution context
balance(a) F wei balance at address a
caller F call sender (excluding delegatecall)
callvalue F wei sent together with the current call
calldataload(p) F call data starting from position p (32 bytes)
calldatasize F size of call data in bytes
calldatacopy(t, f, s) - F copy s bytes from calldata at position f to mem at position t
codesize F size of the code of the current contract / execution context
codecopy(t, f, s) - F copy s bytes from code at position f to mem at position t
extcodesize(a) F size of the code at address a
extcodecopy(a, t, f, s) - F like codecopy(t, f, s) but take code at address a
returndatasize B size of the last returndata
returndatacopy(t, f, s) - B copy s bytes from returndata at position f to mem at position t
extcodehash(a) C code hash of address a
create(v, p, n) F create new contract with code mem[p…(p+n)) and send v wei and return the new address
create2(v, p, n, s) C create new contract with code mem[p…(p+n)) at address keccak256(0xff . this . s . keccak256(mem[p…(p+n))) and send v wei and return the new address, where 0xff is a 8 byte value, this is the current contract’s address as a 20 byte value and s is a big-endian 256-bit value
call(g, a, v, in, insize, out, outsize) F call contract at address a with input mem[in…(in+insize)) providing g gas and v wei and output area mem[out…(out+outsize)) returning 0 on error (eg. out of gas) and 1 on success
callcode(g, a, v, in, insize, out, outsize) F identical to call but only use the code from a and stay in the context of the current contract otherwise
delegatecall(g, a, in, insize, out, outsize) H identical to callcode but also keep caller and callvalue
staticcall(g, a, in, insize, out, outsize) B identical to call(g, a, 0, in, insize, out, outsize) but do not allow state modifications
return(p, s) - F end execution, return data mem[p…(p+s))
revert(p, s) - B end execution, revert state changes, return data mem[p…(p+s))
selfdestruct(a) - F end execution, destroy current contract and send funds to a
invalid - F end execution with invalid instruction
log0(p, s) - F log without topics and data mem[p…(p+s))
log1(p, s, t1) - F log with topic t1 and data mem[p…(p+s))
log2(p, s, t1, t2) - F log with topics t1, t2 and data mem[p…(p+s))
log3(p, s, t1, t2, t3) - F log with topics t1, t2, t3 and data mem[p…(p+s))
log4(p, s, t1, t2, t3, t4) - F log with topics t1, t2, t3, t4 and data mem[p…(p+s))
origin F transaction sender
gasprice F gas price of the transaction
blockhash(b) F hash of block nr b - only for last 256 blocks excluding current
coinbase F current mining beneficiary
timestamp F timestamp of the current block in seconds since the epoch
number F current block number
difficulty F difficulty of the current block
gaslimit F block gas limit of the current block

状态变量存储原理

所有的状态变量(不包含 constant 修饰的常量,也就是说常量不会存在 slot 中)都存储在 Slot(链上的每个合约专属的数据库) 中,是按照状态变量的声明顺序依次存储的

每个 slot 占用 256 个 bit,也就是 32 个字节

下面是各种类型的变量在 Slot 中的存储位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.7.0;
contract A {
struct S {
uint128 a; // slot 0, offset 0
uint128 b; // slot 0, offset 16
uint[2] staticArray; // slot 1、2, offset 0
uint[] dynArray; // slot 3, offset 0
}

uint x; // slot 0, offset 0
uint y; // slot 1, offset 0
S s; // slot 2、3、4、5, offset 0
address addr; // slot 6, offset 0
mapping (uint => mapping (address => bool)) map; // slot 7, offset 0
uint[] array; // slot 8, offset 0
string s1; // slot 9, offset 0
bytes b1; // slot 10, offset 0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"astId": 31,
"contract": "fileA:A",
"label": "s1",
"offset": 0,
"slot": "9",
"type": "t_string_storage"
},
{
"astId": 33,
"contract": "fileA:A",
"label": "b1",
"offset": 0,
"slot": "10",
"type": "t_bytes_storage"
}
]

可以看到上面的 map 只占用了一个 slot,其实很明显不是的,一个 map 数据量可以很大,不是一个 32 字节的 slot 所能装得下的

其实 mapping 的存储原理更加的复杂,它会利用 key 的值以及 slot 的位置来找到一个新的 slot 位置,这个位置中放的就是 key 对应的 value

下面是各种数据类型的存储原理

参考网址:https://medium.com/aigang-network/how-to-read-ethereum-contract-storage-44252c8af925

合约例子

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
/**
*Submitted for verification at Etherscan.io on 2017-12-29
*/

pragma solidity ^0.4.0;

contract testStorage {

uint storeduint1 = 15;
uint constant constuint = 16;
uint128 investmentsLimit = 17055;
uint32 investmentsDeadlineTimeStamp = uint32(now);

bytes16 string1 = 'test1';
bytes32 string2 = 'test1236';
string string3 = 'lets string something';

mapping (address => uint) uints1;
mapping (address => DeviceData) structs1;

uint[] uintarray;
DeviceData[] deviceDataArray;

struct DeviceData {
string deviceBrand;
string deviceYear;
string batteryWearLevel;
}

function testStorage() {
address address1 = 0xbccc714d56bc0da0fd33d96d2a87b680dd6d0df6;
address address2 = 0xaee905fdd3ed851e48d22059575b9f4245a82b04;

uints1[address1] = 88;
uints1[address2] = 99;

var dev1 = DeviceData('deviceBrand', 'deviceYear', 'wearLevel');
var dev2 = DeviceData('deviceBrand2', 'deviceYear2', 'wearLevel2');

structs1[address1] = dev1;
structs1[address2] = dev2;

uintarray.push(8000);
uintarray.push(9000);

deviceDataArray.push(dev1);
deviceDataArray.push(dev2);
}
}

uint

获取 storeduint1 变量的值

1
2
3
4
5
6
7
let contractAddress = '0xf1f5896ace3a78c347eb7eab503450bc93bd0c3b'
let index = 0
console.log(web3.eth.getStorageAt(contractAddress, index))
console.log('DEC:' + web3.toDecimal(web3.eth.getStorageAt(contractAddress, index)))

// result: 0x000000000000000000000000000000000000000000000000000000000000000f
// DEC:15

uint128、uint32

获取 investmentsLimit 变量以及 investmentsDeadlineTimeStamp 变量的值

都是占用 128 个 bit,也就是 16 个字节

1
2
3
4
5
let index = 1
console.log(web3.eth.getStorageAt(contractAddress, index))

// result: 0x00000000000000000000000059b92d9a0000000000000000000000000000429f
// DEC: 1505308058 and 17055

bytes16

获取 string1 的值

1
2
3
4
5
6
index = 2
console.log(web3.eth.getStorageAt(contractAddress, index))
console.log('ASCII: ' + web3.toAscii(web3.eth.getStorageAt(contractAddress, index)))

// result: 0x0000000000000000000000000000000074657374310000000000000000000000
// ASCII: test1

bytes32

获取 string2 的值

1
2
3
4
5
6
index = 3
console.log(web3.eth.getStorageAt(contractAddress, index))
console.log('ASCII: ' + web3.toAscii(web3.eth.getStorageAt(contractAddress, index)))

// result: 0x7465737431323336000000000000000000000000000000000000000000000000
// ASCII: test1236

string

获取 string3 的值

1
2
3
4
5
6
index = 4
console.log(web3.eth.getStorageAt(contractAddress, index))
console.log('ASCII: ' + web3.toAscii(web3.eth.getStorageAt(contractAddress, index)))

// result: 0x6c65747320737472696e6720736f6d657468696e67000000000000000000002a
// ASCII: lets string something * (42)

mapping (address => uint)

获取 uints1 的值

1
2
3
4
5
6
7
8
index = '0000000000000000000000000000000000000000000000000000000000000005'
key = '00000000000000000000000xbccc714d56bc0da0fd33d96d2a87b680dd6d0df6'
let newKey = web3.sha3(key + index, {"encoding":"hex"})
console.log(web3.eth.getStorageAt(contractAddress, newKey))
console.log('DEC: ' + web3.toDecimal(web3.eth.getStorageAt(contractAddress, newKey)))

// result: 0x0000000000000000000000000000000000000000000000000000000000000058
// DEC: 88

mapping (address => DeviceData)

获取 structs1 的值的第一个字段

1
2
3
4
5
6
7
8
index = "0000000000000000000000000000000000000000000000000000000000000006"
key = "00000000000000000000000xbccc714d56bc0da0fd33d96d2a87b680dd6d0df6"
let newKey = web3.sha3(key + index, {"encoding":"hex"})
console.log(web3.eth.getStorageAt(contractAddress, newKey))
console.log('ASCII: ' + web3.toAscii(web3.eth.getStorageAt(contractAddress, newKey)))

// result: 0x6465766963654272616e64000000000000000000000000000000000000000016
// ASCII: deviceBrand

获取 structs1 的值的第二个字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function increaseHexByOne(hex) {
let x = new BigNumber(hex)
let sum = x.add(1)
let result = '0x' + sum.toString(16)
return result
}
index = "0000000000000000000000000000000000000000000000000000000000000006"
key = "00000000000000000000000xbccc714d56bc0da0fd33d96d2a87b680dd6d0df6"
let newKey = increaseHexByOne(
web3.sha3(key + index, {"encoding":"hex"}))
console.log(web3.eth.getStorageAt(contractAddress,newKey))
console.log('ASCII: ' +
web3.toAscii(web3.eth.getStorageAt(contractAddress, newKey)))

// result: 0x6465766963655965617200000000000000000000000000000000000000000014
// ASCII: deviceYear

uint[]

获取 uintarray 的数组成员的个数

1
2
3
4
index = "7"
console.log(web3.eth.getStorageAt(contractAddress, index))

// result: 0x0000000000000000000000000000000000000000000000000000000000000002

获取 uintarray 的数组成员的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
index = "0000000000000000000000000000000000000000000000000000000000000007"
let newKey = web3.sha3(index, {"encoding":"hex"})
console.log(web3.eth.getStorageAt(contractAddress, newKey))
console.log('DEC: ' + web3.toDecimal(web3.eth.getStorageAt(contractAddress, newKey)))

// result: 0x0000000000000000000000000000000000000000000000000000000000001f40
// DEC: 8000

newKey = increaseHexByOne(web3.sha3(index, {"encoding":"hex"}))
console.log(web3.eth.getStorageAt(contractAddress, newKey))
console.log('DEC: ' + web3.toDecimal(web3.eth.getStorageAt(contractAddress, newKey)))

// result: 0x0000000000000000000000000000000000000000000000000000000000002328
// DEC: 9000

DeviceData[]

获取 deviceDataArray 的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
index = "0000000000000000000000000000000000000000000000000000000000000008"
let newKey = web3.sha3(index, {"encoding":"hex"})
console.log(web3.eth.getStorageAt(contractAddress, newKey))
console.log('ASCII: ' + web3.toAscii(web3.eth.getStorageAt(contractAddress, newKey)))

// result: 0x6465766963654272616e64000000000000000000000000000000000000000016
// ASCII: deviceBrand

index = "0000000000000000000000000000000000000000000000000000000000000008"
let newKey = increaseHexByOne(web3.sha3(index, {"encoding":"hex"}))
console.log(web3.eth.getStorageAt(contractAddress, newKey))
console.log('ASCII: ' + web3.toAscii(web3.eth.getStorageAt(contractAddress, newKey)))

// result: 0x6465766963655965617200000000000000000000000000000000000000000014
// ASCII: deviceYear

newKey 再 +1 就是第 1 个成员的第 3 个字段

newKey 再 +1 就是第 2 个成员的第 1 个字段

newKey 再 +1 就是第 2 个成员的第 2 个字段

newKey 再 +1 就是第 2 个成员的第 3 个字段

视频教程地址

敬请期待

pefish学习社区

微信:pefishpefish,加好友,备注pefish学习社区,邀你入群

Telegramhttps://t.me/pefish_study




微信关注我,及时接收最新技术文章