存储规则
智能合约的所有状态变量 - 或持久性变量 - 都驻留在合约的存储中。合约的存储是一个大型数组,组织成大小为32字节(256位)的槽,每个槽由一个唯一的索引标识,范围从0到$2^{256}-1$。对于每个给定的状态变量,存储槽索引必须以确定性的方式计算,以确保对存储数据的一致访问。事实上,这种存储槽中状态变量的布局由编译器确定,编译器考虑源代码中变量的声明顺序(Solidity 编译器为例)。
固定大小的变量类型。基本类型,如uint(32字节)、地址(20字节)和布尔值(1字节)以连续的方式存储在32字节的槽中,从槽0x0开始。值得注意的是,需要少于32字节的多个连续项被打包到单个存储槽中以节省空间,根据以下规则进行打包:
- 存储槽中的第一项始终与到最低有效字节对齐。
- 固定大小的类型仅使用所需的字节来存储它们。
- 如果变量不适合存储槽的剩余部分,则将其存储在下一个存储槽中。
动态大小的变量类型。动态类型,如数组和映射,始终存储在新的槽中(基槽),它们的元素根据以下规则存储:
- 基槽存储数组的长度,或者在映射的情况下,它被留空。
- 每个数组元素(具有索引:INDEX)存储在槽keccak256(BASE)+INDEX中。
- 映射的每个元素(具有键:KEY)存储在通过连接KEY和BASE并将结果值用作keccak256函数的输入计算得到的槽中(即,keccak256(KEY.BASE))。
注意
状态变量在存储中的位置是通过声明状态变量来分配空间的(占坑的)。
通过简单合约举例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
struct Values {
uint8 a;
address b;
bytes4 c;
}
contract Test {
Values value;
uint8 balance;
bytes4 data;
address addr;
uint8[] uint8Array;
mapping(address => uint256) isMapping;
constructor(){
balance = 8;
addr = 0x358AA13c52544ECCEF6B0ADD0f801012ADAD5eE3;
uint8Array.push(1);
uint8Array.push(2);
isMapping [addr] = 123;
}
}
Values结构体总体大小为25字节大小,第一个声明Values value;
占用 Slot 0 这个坑,哪怕没对value进行赋值。
balance
占用8字节,因此无法完全存储在slot 0 剩余大小的空间中,因此balance
开始存储在slot 1的低位。data
占用4字节,存储在slot 1中,而addr
占用20字节,也存储在slot 1中。在构造函数中,被赋值的只有balance
和addr
。
slot1 在remix调试显示结果是:
可以看到两个被赋值的没有紧挨在一起存储,因为中间4字节被data
声明给占坑了。
规则中数组和映射基槽都占用一整个32字节的 slot 作为基槽。
uint8Array
的基槽就是 slot 2,keccak256(BASE)哈希计算出要存储的slot位置,将元素按keccak256(BASE)+INDEX要求存入。
在mapping中,keccak256(KEY.BASE)在EVM中的实现是通过执行keccak256操作码来完成的。这个操作码会消耗堆栈中的两个元素:stack(0)中的值表示要读取的内存起始位置,stack(1)中的值表示要读取字节的大小。
具体来说,memory(0)中缓存的是addr的值,而memory(1)中缓存的是基槽slot 3的值。然后,EVM会读取这两个值连接起来的64字节数据(memory(0)的32字节 + memory(1)的32字节),并对连接后的值进行哈希计算。
执行完后,isMapping [addr] = 123
在storage中的体现是:
如果有任何错误,请指正。