Q1ngying

今朝梦醒与君别,遥盼春风寄相思

0%

Storage 进阶——通过引用 Storage 提高 gas 利用率

本文主要介绍了直接在函数中 “引用” Storage;ERC-7201:命名空间存储布局,通过 assembly 在我们想要的 slot 位置定义状态变量

Storage 高效利用——引用 Storage

先看例子:

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
    struct Proposal {
address proposer;
address recipient;
bytes32 payloadHash;
uint256 votes;
uint256 beginBlock;
mapping(address => bool) voted;
bool executed;
}
Proposal[] public proposals;

constructor(R3Token _token) {
token = _token;
}

function propose(address recipient, bytes32 payloadHash) external returns (uint256) {
uint256 proposalIndex = proposals.length;
proposals.push();
@> Proposal storage proposal = proposals[proposalIndex];
proposal.proposer = msg.sender;
proposal.recipient = recipient;
proposal.payloadHash = payloadHash;
proposal.beginBlock = block.number - 1;
return proposalIndex;
}

在上面的例子propose函数中,Proposal storage proposal = proposals[proposalIndex] 这行表示的意识是对状态变量proposals[proposalIndex]对应的自定义类型Proposal引用*我最开始认为的是定义状态变量,但是我自己都觉得不对……*。这是一种对 Solidity Storage 结构的高效利用。如果不知道这种利用方式的情况下,我们常见的方法是:

  • 定义一个 Memory 存储方式的 Proposal 结构体(开辟 EVM Memory 空间)
  • 为这个结构体赋值
  • 将这个结构体存储到 Storage 中(写入 Storage)

而这里运用了 引用 Storage 的方法来向状态变量proposals中添加新的成员变量:

  • uint256 proposalIndex = proposals.length获取proposals长度
  • proposals.push()开辟新的数组成员
  • Proposal storage proposal = proposals[proposalIndex]引用 Storage 中,proposals[proposalIndex] 对应元素的Proposal自定义类型
    • 这里涉及到一个细节:数组的 Index 是从0开始的,也就是说,实际上proposals[proposals.length]这个位置是没有被定义的(换种说法,假设数组中有三个元素,数组长度为3,但是数组最后一个元素的索引其实是2
  • 后续便是直接为proposals[proposalIndex]对应的元素进行赋值

对比上述两种方法我们可以看到,下面这种引用 Storage 来为状态变量进行赋值的方法,直接操纵了 Storage,比传统的先在 Memory 中定义要存储的值再存储到 Storage 更节省 gas。(因为第一种方法额外开辟了 Memory)

实际上,在函数中,storage关键字都表示对状态变量(Storage)中的变量的引用

通过内联汇编assembly操纵 Storage

OpenZeppelin OwnableUpgradeable.sol 合约中(节省空间,我去掉了注释):

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
pragma solidity ^0.8.20;

import {ContextUpgradeable} from "../utils/ContextUpgradeable.sol";
import {Initializable} from "../proxy/utils/Initializable.sol";

abstract contract OwnableUpgradeable is Initializable, ContextUpgradeable {
/// @custom:storage-location erc7201:openzeppelin.storage.Ownable
struct OwnableStorage {
address _owner;
}

// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant OwnableStorageLocation = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300;

function _getOwnableStorage() private pure returns (OwnableStorage storage $) {
assembly {
$.slot := OwnableStorageLocation
}
}

error OwnableUnauthorizedAccount(address account);
error OwnableInvalidOwner(address owner);

event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

function __Ownable_init(address initialOwner) internal onlyInitializing {
__Ownable_init_unchained(initialOwner);
}

function __Ownable_init_unchained(address initialOwner) internal onlyInitializing {
if (initialOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(initialOwner);
}

modifier onlyOwner() {
_checkOwner();
_;
}

function owner() public view virtual returns (address) {
OwnableStorage storage $ = _getOwnableStorage();
return $._owner;
}

function _checkOwner() internal view virtual {
if (owner() != _msgSender()) {
revert OwnableUnauthorizedAccount(_msgSender());
}
}

function renounceOwnership() public virtual onlyOwner {
_transferOwnership(address(0));
}

function transferOwnership(address newOwner) public virtual onlyOwner {
if (newOwner == address(0)) {
revert OwnableInvalidOwner(address(0));
}
_transferOwnership(newOwner);
}

function _transferOwnership(address newOwner) internal virtual {
OwnableStorage storage $ = _getOwnableStorage();
address oldOwner = $._owner;
$._owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}

这里有几个很有意思的点:

  • Solidity 是允许将变量名称定义为$
  • Solidity 允许我们通过assembly内联汇编代码块直接定义状态变量,并将其定义在我们想要的slot位置(这点在本文后面我们将展开讨论).
  • Solidity 内联汇编的slot指针$.slot$是变量名称)

“另类”的定义状态变量方法(ERC-7201:命名空间存储布局)

这个库合约,为了避免合约升级时发生存储冲突,采用了命名空间存储布局的方法来定义状态变量

首先,它按照 ERC-7201:命名空间存储布局计算了变量 Ownable实际应该存储的 slot 位置:

1
2
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant OwnableStorageLocation = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300;

_getOwnableStorage()函数中,他直接通过assembly内联汇编代码块,将变量$指向OwnableStorage实际存储位置,这个函数的返回值是**OwnableStorage结构体(Storage)的引用**

1
2
3
4
5
function _getOwnableStorage() private pure returns (OwnableStorage storage $) {
assembly {
$.slot := OwnableStorageLocation
}
}

而当用户需要读取Owner是谁的时候,可以通过调用owner()函数:

  • 首先,调用内部函数_getOwnableStorage()获得了结构体OwnableStorage(状态变量)的引用
  • 然后返回了结构体$OwnableStorage)的_owner属性(方法)。
1
2
3
4
function owner() public view virtual returns (address) {
OwnableStorage storage $ = _getOwnableStorage();
return $._owner;
}

我们再次观察 OpenZeppelin 的合约,实际上,他并没有在高级语言层面(solidity 层面)定义状态变量OwnableStorage。它是运用内联汇编(EVM 层面)直接为对应的 slot 赋值,也就是说,在 solidity 层面,solidity 是不知道状态变量OwnableStorage的存在的,这边需要我们使用assembly内敛汇编代码块,将变量指向对应的 slot ($.slot := ...)。