Storage 进阶——通过引用 Storage 提高 gas 利用率
Storage 高效利用——引用 Storage
先看例子:
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
)
- 这里涉及到一个细节:数组的 Index 是从
- 后续便是直接为
proposals[proposalIndex]
对应的元素进行赋值
对比上述两种方法我们可以看到,下面这种引用 Storage 来为状态变量进行赋值的方法,直接操纵了 Storage,比传统的先在 Memory 中定义要存储的值再存储到 Storage 更节省 gas。(因为第一种方法额外开辟了 Memory)
实际上,在函数中,storage
关键字都表示对状态变量(Storage)中的变量的引用
通过内联汇编assembly
操纵 Storage
在 OpenZeppelin OwnableUpgradeable.sol
合约中(节省空间,我去掉了注释):
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
指针(是变量名称)
“另类”的定义状态变量方法(ERC-7201:命名空间存储布局)
这个库合约,为了避免合约升级时发生存储冲突,采用了命名空间存储布局的方法来定义状态变量。
首先,它按照 ERC-7201:命名空间存储布局计算了变量 Ownable
实际应该存储的 slot 位置:
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Ownable")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant OwnableStorageLocation = 0x9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300;
在_getOwnableStorage()
函数中,他直接通过assembly
内联汇编代码块,将变量$
指向OwnableStorage
实际存储位置,这个函数的返回值是OwnableStorage
结构体(Storage)的引用
function _getOwnableStorage() private pure returns (OwnableStorage storage $) {
assembly {
$.slot := OwnableStorageLocation
}
}
而当用户需要读取Owner
是谁的时候,可以通过调用owner()
函数:
- 首先,调用内部函数
_getOwnableStorage()
获得了结构体OwnableStorage
(状态变量)的引用 - 然后返回了结构体
$
(OwnableStorage
)的_owner
属性(方法)。
function owner() public view virtual returns (address) {
OwnableStorage storage $ = _getOwnableStorage();
return $._owner;
}
我们再次观察 OpenZeppelin 的合约,实际上,他并没有在高级语言层面(solidity 层面)定义状态变量OwnableStorage
。它是运用内联汇编(EVM 层面)直接为对应的 slot 赋值,也就是说,在 solidity 层面,solidity 是不知道状态变量OwnableStorage
的存在的,这边需要我们使用assembly
内敛汇编代码块,将变量指向对应的 slot ($.slot := ...
)。