合约变量存储机制

合约变量存储机制

介绍

我们可以在智能合约上永久存储数据。每个智能合约都在自己的永久存储中维护其状态。它的作用类似于“智能合约的小型数据库”,但与其他数据库不同,该数据库是可公开访问的。存储在智能合约存储中的所有值都可供外部免费读取(通过静态调用),无需向区块链发送交易。

因为零不占用任何空间,所以可以通过将值设置为零来回收存储。当您将值更改为零时,这会在智能合约中通过gas 退款得到激励。

智能合约存储是一个key-value映射(=数据库),其中key对应存储中的一个槽号,value是这个存储槽中存储的实际值。

智能合约的存储由插槽组成,其中:

  • 每个存储槽可以包含长达 32 个字节的字。
  • 存储槽从位置 0 开始(如数组索引)
  • 总共有 2²⁵⁶ 存储插槽可用(用于读/写)
  • 存储槽的第一项以低位对齐(lower-order aligned)的方式存储
  • immutable 和 constant 不参与

有一个优化存储原则:如果下一个变量长度和上一个变量长度加起来不超过256bits,它们就会存储在同一个插槽里

可以使用 web3.eth.getStorageAt 来读取相应slot的内容

也可以在合约中编写函数来查看

1
2
3
4
5
function readStorageSlot0() public view returns (bytes32 result) {
assembly {
result := sload(0)
}
}

也可以在区块链浏览器或remix的debug中查看

对于使用继承的合约,状态变量的顺序由没有任何其他合约依赖的合约开始的 C3 线性顺序(C3-linearized order)决定。如果上述规则允许的话,不同合约的状态变量共享同一个存储槽。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
contract A {
    uint a = 1; // slot 0 for contract D. slot 2 for contract E.
    uint128 b = 2; // slot 1 for contract D slot 3 for contract E.
    uint128 c = 3;
}
contract B {
    uint d = 4; // slot 2 for contract D. slot 0 for contract E.
    uint e = 5; // slot 3 for contract D. slot 1 for contract E.
}
contract D is A, B {
    uint f = 6; // slot 4
}
contract E is B, A {
    uint f = 6; // slot 4
}

参考1

参考2-Storage官方文档

前提知识

1字节:0x00、uint8、bool、bytes1

20字节:address

固定大小的值

简单变量

即值类型

  • bool:可以保存 true 或 false 作为其值的布尔值
  • uint:这是无符号整数,只能保存0和正值
  • int:这是可以保存负值和正值的有符号整数
  • address:这表示以太坊环境中的账户地址
  • byte:这表示固定大小的字节数组(byte1bytes32

一些关于byte的知识

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
pragma solidity ^0.6.0;
contract StorageContract {
uint256 a = 10;
uint64 b = 20;
uint8 c = 30;
int128 d = 40;
bool e = false;
bytes1 f = 0x10;
address g = 0x80ec8696D724686adCC88fFF14Bde24A4d0e38De;
function readStorageSlot0() public view returns (bytes32 result) {
assembly {
result := sload(0)
}
}
function readStorageSlot1() public view returns (bytes32 result) {
assembly {
result := sload(1)
}
}
function readStorageSlot2() public view returns (bytes32 result) {
assembly {
result := sload(2)
}
}
function readStorageSlot3() public view returns (bytes32 result) {
assembly {
result := sload(3)
}
}
}

image-20220807185513619

分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//slot0
0x000000000000000000000000000000000000000000000000000000000000000a
uint256 a=10

//slot1
0x00000000001000000000000000000000000000000000281e0000000000000014
0x0000000000000014 uint64 b=20
0x1e uint8 c=30
0x00000000000000000000000000000028 uint128 d=40
0x00 bool e=false
0x10 bytes1 f=0x10

//slot2
0x00000000000000000000000080ec8696d724686adcc88fff14bde24a4d0e38de
address g

定长数组

定长数组即事先规定好长度的数组,和简单变量类似

1
2
3
4
5
6
7
8
9
10
11
12
13
contract arrayContract {
bytes8[5] a = [byte(0x6a),0x68,0x79,0x75];
function readStorageSlot0() public view returns (bytes32 result) {
assembly {
result := sload(0)
}
}
/*
function add() public{
a.push(0x99);
}
*/
}

image-20220808215319958

在storage中存储后,后续想要在数组中添加是不被允许的

image-20220808215215380

结构体

结构体Struct也是类似

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
contract arrayContract {
struct Entry {
byte id;
byte value;
}

bytes4[5] a = [byte(0x6a),0x68,0x79,0x99];
bool b = true;
Entry c = Entry({id:0x25,value:0x98});

function Slot0() public view returns (bytes32 result) {
assembly {
result := sload(0)
}
}
function Slot1() public view returns (bytes32 result) {
assembly {
result := sload(1)
}
}
function Slot2() public view returns (bytes32 result) {
assembly {
result := sload(2)
}
}
/*
function add() public{
a.push(0x99);
}
*/
}

image-20220808225505324

动态大小的值

Solidity 使用散列函数来统一且可重复地计算动态大小值的位置。

动态数组

1
2
3
4
5
6
7
contract TEST{
uint[] a=[0x77,0x88,0x99];
function add() public{
a.push(0x66);
}

}

image-20220808230350960

如何计算数据存储的第一个slot,把储存数组长度的位置进行keccak_256

1
2
3
4
5
6
7
8
9
10
11
12
13
import binascii

from _pysha3 import keccak_256


def byte32(i):
return binascii.unhexlify('%064x'%i)

//用数组长度存储的位置进行计算
a=keccak_256(byte32(0)).hexdigest()
print(a)

#290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563

后面的数据存储的slot依次+1

映射

1
2
3
4
5
6
contract MAPPING {
mapping(uint256 => uint256) a;
function add() public {
a[123] = 456;
}
}

image-20220808233453422

将mapping的key和slot一起keccak_256

注意,mapping的位置实际上没有存储任何内容。(没有要存储的长度,单个值需要位于其他位置。)

1
2
3
4
5
6
7
8
9
10
11
12
13
import binascii

from _pysha3 import keccak_256


def byte32(i):
return binascii.unhexlify('%064x'%i)


b=keccak_256(byte32(123)+byte32(0)).hexdigest()
print(b)

#de31a920dbdd1f015b2a842f0275dc8dec6a82ff94d9b796a36f23c64a3c8332

如果映射值是一个非值类型,计算槽位置标志着数据的开始位置。例如,如果值是结构类型,你必须添加一个与结构成员相对应的偏移量才能到达该成员(offset)。

1
2
3
4
5
6
7
8
9
10
contract C {
    struct S { uint16 a; uint16 b; uint256 c; }
    uint x;
    mapping(uint => mapping(uint => S)) data;
}

// 计算 data[4][9].c 的存储位置
// data[4]: keccak256(byte32(4) + byte32(1))
// data[4][9]: keccak256(byte32(9) + keccak256(byte32(4) + byte32(1))
// data[4][9].c: keccak256(byte32(9) + keccak256(byte32(4) + byte32(1)) + 1

案例分析

Ethernaut闯关 12.Privacy

Ethernaut闯关 19.Alien Codex


合约变量存储机制
http://sissice.github.io/2022/08/09/合约变量存储机制/
作者
Sissice
发布于
2022年8月9日
许可协议