合约变量存储机制 介绍 我们可以在智能合约上永久存储数据。每个智能合约都在自己的永久存储中维护其状态。它的作用类似于“智能合约的小型数据库”,但与其他数据库不同,该数据库是可公开访问的。存储在智能合约存储中的所有值都可供外部免费读取(通过静态调用),无需向区块链发送交易。
因为零不占用任何空间,所以可以通过将值设置为零来回收存储。当您将值更改为零时,这会在智能合约中通过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
:这表示固定大小的字节数组(byte1
到 bytes32
)
一些关于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) } } }
分析一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0x000000000000000000000000000000000000000000000000000000000000000a uint 256 a=10 0x00000000001000000000000000000000000000000000281e0000000000000014 0x0000000000000014 uint 64 b=20 0x1e uint 8 c=30 0x00000000000000000000000000000028 uint 128 d=40 0x00 bool e=false 0x10 bytes1 f=0x10 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); } */ }
在storage中存储后,后续想要在数组中添加是不被允许的
结构体 结构体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); } */ }
动态大小的值 Solidity 使用散列函数来统一且可重复地计算动态大小值的位置。
动态数组 1 2 3 4 5 6 7 contract TEST{ uint[] a=[0x77,0x88,0x99]; function add() public{ a.push(0x66); } }
如何计算数据存储的第一个slot,把储存数组长度的位置进行keccak_256
1 2 3 4 5 6 7 8 9 10 11 12 13 import binasciifrom _pysha3 import keccak_256def byte32 (i ): return binascii.unhexlify('%064x' %i) //用数组长度存储的位置进行计算 a=keccak_256(byte32(0 )).hexdigest()print (a)
后面的数据存储的slot依次+1
映射 1 2 3 4 5 6 contract MAPPING { mapping (uint256 => uint256) a; function add () public { a[123 ] = 456 ; } }
将mapping的key和slot一起keccak_256
注意,mapping的位置实际上没有存储任何内容。(没有要存储的长度,单个值需要位于其他位置。)
1 2 3 4 5 6 7 8 9 10 11 12 13 import binasciifrom _pysha3 import keccak_256def byte32 (i ): return binascii.unhexlify('%064x' %i) b=keccak_256(byte32(123 )+byte32(0 )).hexdigest()print (b)
如果映射值是一个非值类型,计算槽位置标志着数据的开始位置。例如,如果值是结构类型,你必须添加一个与结构成员相对应的偏移量才能到达该成员(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