本文主要介绍了直接在函数中 “引用” 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 := ...
)。