1. 引言
前序博客有:
- Ethereum EVM简介
- 揭秘EVM Opcodes
- 剖析Solidity合约创建EVM bytecode
Solidity底层通过SLOAD和SSTORE opcode来控制EVM storage。
2. 何为Storage?
Storage为每个合约的持久mapping,具有 2 256 − 1 2^{256}-1 2256−1个32 byte words。当在合约中设置某状态变量值时,其会存储在指定的slot中,其将持续在EVM中,除非被相同类型的其它值覆盖。
3. 何时用Storage?何时用Memory?
当首次加载某storage slot时,其是cold的,意味着需要2100 gas,后续再调用该slot时,其是warm的,仅需100 gas。而Memory更便宜,其低至3 gas(当有memory expansion时,将更贵点)。
举例如下,未优化合约:
contract C {
struct S {
uint256 a;
uint256 b;
address c;
}
S public s;
function foo(uint256 input) external {
// `s.b` is loaded from storage once: warming up the storage!
if (input < s.b) revert;
// Second `s.b` SLOAD with warm storage.
if (s.b > 50) revert;
}
其中s,b
从storage中加载了2次。可优化为:创建内存变量来存储s.b
值,后续使用该内存变量。原因在于MLOAD比SLOAD便宜。即优化为:
function foo(uint256 input) external {
// Initial storage load to store in memory.
uint256 b = s.b;
// Using MLOAD in comparison operations!
if (input < b) revert;
if (b > 50) revert;
}
4. 手工分配Storage
// SPDX-License-Identifier: MIT
pragma solidity 0.8.6;
contract C {
struct S {
uint16 a; // 2 bytes, 2 bytes total
uint24 b; // 3 bytes, 5 bytes total
address c; // 20 bytes, 25 bytes total + end of slot 0x01
address d; // 20 bytes, slot 0x02
}
// I've noted the storage slots each state is located at.
// A single slot is 32 bytes :)
uint256 boring; // 0x00
S s_struct; // 0x01, 0x02
S[] s_array; // 0x03
mapping(uint256 => S) s_map; // 0x04
constructor() {
boring = 0x288;
s_struct = S({
a: 10,
b: 20,
c: 0xdcD49C36E69bF85FA9c5a25dEA9455602C0B289e,
d: 0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263
});
s_array.push(s_struct);
s_array.push(s_struct);
}
function view_boring() external view returns (bytes32) {
bytes32 x;
assembly {
x := sload(0x00)
}
return x;
}
function view_slot(uint256 slot) external view returns(bytes32) {
bytes32 x;
assembly {
x := sload(slot)
}
return x;
}
function view_b() external view returns (uint256) {
bytes32 x;
assembly {
// before: 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014 000a
// ^
// after: 0000 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014
// ^
let v := shr(0x10, sload(0x01))
// If both characters aren't 0, keep the bit (1). Otherwise, set to 0.
// mask: 0000000000000000000000000000000000000000000000000000000000 FFFFFF
// v: 000000000000000000dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014
// result: 0000000000000000000000000000000000000000000000000000000000 000014
v := and(0xffffff, v)
// Store in memory bc return uses memory.
mstore(0x40, v)
// Return reads left to right.
// Since our value is far right we can just return 32 bytes from the 64th byte in memory.
x := mload(0x40)
}
return uint256(x);
}
// unused bytes c b a
// before: 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014 000a
// unused bytes c b a
// after: 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 0001F4 000a
function set_b(uint24 b) external {
assembly {
// Removing the `uint16` from the right.
// before: 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014 000a
// ^
// after: 0000 00000000000000 dcd49c36e69bf85fa9c5a25dea9455602c0b289e 000014
// ^
let new_v := shr(0x10, sload(0x01))
// Create our mask.
new_v := and(0xffffff, new_v)
// Input our value into the mask.
new_v := xor(b, new_v)
// Add back the removed `a` value bits.
new_v := shl(0x10, new_v)
// Replace original 32 bytes' `000014` with `0001F4`.
new_v := xor(new_v, sload(0x01))
// Store our new value.
sstore(0x01, new_v)
}
}
function view_d() external view returns (address) {
return s_array[0].d;
}
// keccak256(array_slot) + var_slot
// keccak256(0x03) + 1
// Remember how `s_struct` takes up 2 slots?
// The `+ 1` indicates the second slot allocation in S
// For the bitpacked slot in S we use don't need the add
// The next element's slot would be `+ 2`
function get_element() external view returns(bytes32) {
bytes32 x;
assembly {
// Store array slot in memory.
mstore(0x0, 0x03)
// Keccak does the MLOAD internally so we give the memory location.
let hash := add(keccak256(0x0, 0x20), 1)
// Store the return value.
mstore(0x0, sload(hash))
// Return `d`.
x := mload(0x0)
}
return x;
}
// 返回s_array数组的长度
function s_arrayLength () public view returns (uint r) {
assembly {
r := sload (3)
}
}
// 从storage中取s_array数组的第i个32字节内容。
function s_arrayElement (uint i) public view returns (bytes32 r) {
assembly {
mstore (0, 3)
r := sload (add (keccak256 (0, 32), i))
}
}
}
4.1 基础类型访问
当想要访问uint256 boring
时,访问方式可为:
assembly {
let x := sload(0x00)
}
4.2 访问Bitpacked结构体
所谓bitpacked,是指在单个slot(32 bytes)内存储了多个变量。在本例中,slot 0x01
中pack了共25字节内容:
-
uint16 a
(2 bytes). -
uint24 b
(3 bytes). -
address c
(20 bytes).
对应s_struct
的slots为:
// 0x01 0x00000000000000dcd49c36e69bf85fa9c5a25dea9455602c0b289e000014000a
// 0x02 0x0000000000000000000000004675c7e5baafbffbca748158becba61ef3b0a263
Bitpacked结构体的查询和设置,可参看上面合约中的view_b
和set_b
。
4.3 访问数组结构
pragma solidity >=0.7.0 <0.9.0;
contract Foo {
uint internal x; // Storage slot #0
mapping (uint => uint) internal y; // Storage slot #1
uint [] internal z; // Storage slot #2
constructor() {
z.push(8);
z.push(9);
}
// 动态数组的长度
function zLength () public view returns (uint r) {
assembly {
r := sload (2)
}
}
// 动态数组中第i个元素的值
function zElement (uint i) public view returns (uint r) {
assembly {
mstore (0, 2)
r := sload (add (keccak256 (0, 32), i))
}
}
}
4.4 访问mapping结构
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Foo {
uint internal x; // Storage slot #0
mapping (uint => address) internal y; // Storage slot #1
uint [] internal z; // Storage slot #2
constructor() {
y[10] = 0xdcD49C36E69bF85FA9c5a25dEA9455602C0B289e;
y[3] = 0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263;
}
// 本例中,参数取(10, 1)或(3, 1)可分别获得y[10]以及y[3]的值
function getStorageValue(uint num, uint slot) public view returns (address result) {
assembly {
// Store num in memory scratch space (note: lookup "free memory pointer" if you need to allocate space)
mstore(0, num)
// Store slot number in scratch space after num
mstore(32, slot)
// Create hash from previously stored num and slot
let hash := keccak256(0, 64)
// Load mapping value using the just calculated hash
result := sload(hash)
}
}
}
4.5 访问String和Bytes结构
bytes和string中的元素在storage中均以ASCII码值表示。文章来源:https://www.toymoban.com/news/detail-810101.html
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Foo {
uint internal x; // Storage slot #0
mapping (uint => address) internal y; // Storage slot #1
uint [] internal z; // Storage slot #2
bytes internal b; // Storage slot #3
string internal s; // Storage slot #4
constructor() {
b = "0123456789012345678901234567890123456789";
s = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
}
// bytes和string中的元素在storage中均以ASCII码值表示。
// bytes长度,本例中,返回值为81
function bLength () public view returns (uint r) {
assembly {
r := sload (3)
}
}
// 返回bytes中第i个32字节值
//i=0,对应返回:0x3031323334353637383930313233343536373839303132333435363738393031
//i=1,对应返回:0x3233343536373839000000000000000000000000000000000000000000000000
function bElement (uint i) public view returns (bytes32 r) {
assembly {
mstore (0, 3)
r := sload (add (keccak256 (0, 32), i))
}
}
// string长度,本例中,返回值为105
function sLength () public view returns (uint r) {
assembly {
r := sload (4)
}
}
// 返回string中的第i个32字节值
//i=0,对应返回:0x6162636465666768696a6b6c6d6e6f707172737475767778797a414243444546
//i=1,对应返回:0x4748494a4b4c4d4e4f505152535455565758595a000000000000000000000000
function sElement (uint i) public view returns (bytes32 r) {
assembly {
mstore (0, 4)
r := sload (add (keccak256 (0, 32), i))
}
}
}
参考资料
[1] A Low-Level Guide To Solidity’s Storage Management
[2] Layout of State Variables in Storage
[3] How to get access to the storage array through the solidity assembler?
[4] How to get access to the storage mapping through the solidity assembler?
[5] Storage and memory layout of strings文章来源地址https://www.toymoban.com/news/detail-810101.html
到了这里,关于Solidity Storage底层管理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!