3. Storage
在Ethereum中所有的合约账户能够将数据持久的存储在Storage
中,Storage
的成本要比Memory
贵很多,因为交易执行后,所有的以太坊都要更新合约的Storage。
我们可以将Storage视作一个天文数字般的数组,最初充满了零。数组中每个值都是32字节,有$2^256$这样的值,智能合约可以在任何位置读取或者写入值 ![[Pasted image 20240629131724.png]] 首先我们先记住几个基本概念:
- 每个智能合约都有 $2^256$ 个32 字节值的数组形式存储,全部初始化为零。
- 当我们设置状态变量的值时,他会将其分配在slot中。
- 0不是显式存储的
- Solidity将固定大小的值定位在slot中从slot0开始,比如uint256,address....
- Solidity 利用存储的 稀疏性和哈希输出的均匀分布来安全地定位动态大小的值。
我们来看一个例子:
contract StorageTest {
uint256 a;
uint256[2] b;
struct Entry {
uint256 id;
uint256 value;
}
Entry c;
}
在上面的代码中:
a
会被存储在slot0中- b存储在slot1和2中,因为b是一个数组,同时长度是定好的2,所以会分配两个slot
- c会从slot3开始,并且消耗两个slot,因为Entry结构存储了两个32字节的值
我们可以使用Foundry的inspect来查看布局:
forge inspect StorageTest storage
{
"storage": [
{
"astId": 47406,
"contract": "src/Test.sol:StorageTest",
"label": "a",
"offset": 0,
"slot": "0",
"type": "t_uint256"
},
{
"astId": 47410,
"contract": "src/Test.sol:StorageTest",
"label": "b",
"offset": 0,
"slot": "1",
"type": "t_array(t_uint256)2_storage"
},
{
"astId": 47418,
"contract": "src/Test.sol:StorageTest",
"label": "c",
"offset": 0,
"slot": "3",
"type": "t_struct(Entry)47415_storage"
}
],
"types": {
"t_array(t_uint256)2_storage": {
"encoding": "inplace",
"label": "uint256[2]",
"numberOfBytes": "64",
"base": "t_uint256"
},
"t_struct(Entry)47415_storage": {
"encoding": "inplace",
"label": "struct StorageTest.Entry",
"numberOfBytes": "64",
"members": [
{
"astId": 47412,
"contract": "src/Test.sol:StorageTest",
"label": "id",
"offset": 0,
"slot": "0",
"type": "t_uint256"
},
{
"astId": 47414,
"contract": "src/Test.sol:StorageTest",
"label": "value",
"offset": 0,
"slot": "1",
"type": "t_uint256"
}
]
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32"
}
}
}
对于不同的类型,Solidity的规范如下:
Kind | Declaration | Value | Location |
---|---|---|---|
Simple Variable | T v | v | v's slot |
Fixed-size array | T[10] v | v[n] | (v's slot) + n * (size of T) |
Dunamic array | T[] v | v[n] v.length | keccak256(v's slot) + n * (size of T) v's slot |
Mapping | Mapping(T1 => T2) v | v[key] | keccak256(key . (v's slot)) |
Slot Packing
Solidity 编译器知道它可以在存储槽中存储 32 字节的数据。当我们定了一个uint32类型的value1的时候,value1仅占用 4 个字节存储在槽slot0 时,编译器读取下一个变量时会查看是否可以将其打包到当前存储槽中。所以如果value2是uint128的话也会打包保存在slot0。
比如如下的代码:
contract StorageTest {
uint32 value1;
uint32 value2;
uint64 value3;
uint128 value4;
}
他的存储布局会是什么样的?
contract StorageTest {
uint32 value1; // 4 bytes slot0
uint32 value2; // 4 bytes slot0
uint64 value3; // 8 bytes slot0
uint128 value4;// 16 bytes slot0
}
因为一个Slot占据了32个bytes如果可以打包的话,就会进行打包。
Storage Opcodes
关于Storage
的Opcode
主要有两个SSTORE
和SLOAD
SSTORE:
它从调用堆栈中获取 32 字节key和 32 字节value,并将该 32 字节value存储在该 32 字节Key指代的位置。e.g:
Input | |
---|---|
1 | 0 |
2 | 0xFFFF |
Storage结果:
Storage key after input 1 | Storage value |
---|---|
0 | 0xFFFF |
SLOAD:
它从Stack
中获取 32 字节key
,并将存储在该 32 字节key
位置的 32 字节Value
推送到Stack
上。e.g:
假设目前的Storage如下:
Storage key | Storage Value |
---|---|
0 | 46 |
那么结果就是:
Input | Output | |
---|---|---|
1 | 0 | 46 |