跳到主要内容

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的规范如下:

KindDeclarationValueLocation
Simple VariableT vvv's slot
Fixed-size arrayT[10] vv[n](v's slot) + n * (size of T)
Dunamic arrayT[] vv[n]
v.length
keccak256(v's slot) + n * (size of T)
v's slot
MappingMapping(T1 => T2) vv[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

关于StorageOpcode主要有两个SSTORESLOAD

SSTORE:

它从调用堆栈中获取 32 字节key和 32 字节value,并将该 32 字节value存储在该 32 字节Key指代的位置。e.g:

Input
10
20xFFFF

Storage结果:

Storage key after input 1Storage value
00xFFFF

SLOAD:

它从Stack中获取 32 字节key,并将存储在该 32 字节key位置的 32 字节Value推送到Stack上。e.g:

假设目前的Storage如下:

Storage keyStorage Value
046

那么结果就是:

InputOutput
1046