Pre:
Upgradability patterns in Solidity — Part 2 学习记录大部分机翻+自己进行demo测试原作者示例代码github地址 我的调试代码github地址
存储模式:
接着part1结尾的出现存储冲突 的问题,我们可以通过采用良好的存储模式来避免存储冲突。
三种流行的模式是继承存储、非结构化存储和永久存储(Inherited Storage, Unstructured Storage, and Eternal Storage) 。
继承存储:
避免状态冲突的一种方法是在逻辑和代理合约之间保持相同的存储顺序。让我们检查一个示例实现以获得更好的理解。
Proxy.sol:
Proxy.sol 是一个简单的代理合约,它有一个 upgradeTo 方法,这是一个回退方法。该合约的状态变量存在于 CommonStorage.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 pragma solidity ^0.8 .0 ; import "./CommonStorage.sol" ;contract Proxy is CommonStorage { constructor ( ) { owner = msg.sender ; } function upgradeTo (address _implementation ) public { require (msg.sender == owner, "Only Owner" ); implementation = _implementation; } fallback () external{ implementation = getImplementationAddress (); assembly { let ptr :=mload (0x40 ) calldatacopy (ptr, 0 , calldatasize ()) let result := delegatecall ( gas (), sload (implementation.slot ), ptr, calldatasize (), 0 , 0 ) returndatacopy (ptr, 0 , returndatasize ()) switch result case 0 { revert (ptr, returndatasize ()) } default { return (ptr, returndatasize ()) } } } }
CommonStorage.sol:
CommonStorage 合约由代理合约和逻辑合约共享。CommonStorage 合约还提供了一个公共方法来查看存储合约内部存储的当前实现地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pragma solidity ^0.8 .0 ; contract CommonStorage { address internal implementation; address internal owner; string internal firstName; string internal lastName; function getImplementationAddress ( ) public view returns (address ){ return implementation; } }
LogicV1.sol & LogicV2.sol:
在这个实现中有两个逻辑合约。LogicV1 合约具有用户名的访问器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 pragma solidity ^0.8 .0 ; import "./CommonStorage.sol" ;contract LogicV1 is CommonStorage { function getFirstName ( ) external view returns (string memory ){ return firstName; } function getLastName ( ) external view returns (string memory ){ return lastName; } function setFirstName (string calldata _firstName ) external { firstName = _firstName; } function setLastName (string calldata _lastName ) external { lastName = _lastName; } }
如下所示,LogicV2 合约扩展了 LogicV1 合约,后者又扩展了 CommonStorage 合约。由于继承,LogicV2 合约已经知道其父合约的状态变量。LogicV2 合约还为用户的年龄添加了新的状态变量和访问器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pragma solidity ^0.8 .0 ; import "./LogicV1.sol" ;contract LogicV2 is LogicV1 { uint256 internal age; function getAge ( ) external view returns (uint256 ){ return age; } function setAge (uint256 _age ) external { age = _age; } }
最后,下面的测试用例利用这个实现来表明在之前的实现中遇到的状态冲突现在已经解决了。
测试用例:
测试用例
测试结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Token contract test Proxy try to upgrade ImplementationAddress To LogicV1.address [*]test_proxy_pattern_implementation_equals_to_logic_address Proxy setImplementationAddress successfully [*]test_can_set_and_get_names_from_logicv1 firstName: John lastName: Doe [*]test_can_set_value_from_proxy Proxy try to upgrade ImplementationAddress To LogicV2.address [*]test_can_set_and_get_names_from_logicv2 firstName: Paul lastName: Walker [*]test_can_get_and_set_age_from_proxy ✔ (2274ms) 1 passing (2s) ✨ Done in 6.76s.
继承存储方法通过为代理和逻辑合约所需的状态变量提供严格的存储顺序来解决存储冲突 。
代理合约正在委托对逻辑合约的调用;因此,只有代理合约的存储在使用中。
继承公共存储合约的代理合约可以访问其父合约的所有状态变量。每个状态变量根据其索引占用适当的内存位置。LogicV1 合约也继承了通用存储合约,它知道状态变量是如何存储在代理合约内部的。
当用户尝试设置 firstName 变量时,第三个内存槽在代理合约中被修改。同样,当代理的所有者尝试升级合约时,第一个内存槽被修改。
请注意,LogicV2 合约继承了之前的 LogicV1合约并指定了一个名为 age 的状态变量。在 LogicV2 中继承 LogicV1 通过保持先前声明的状态变量的顺序来防止冲突。需要继承旧合约以防止存储冲突。
如果 LogicV2 合约不继承 LogicV1 合约,age 变量将修改代理合约中的第一个存储槽。
这会在实现变量和年龄变量之间产生冲突。
缺点:
虽然继承存储通过可升级合约解决了存储冲突问题,但这种方法也有其自身的缺点。
由于需要将所有先前声明的状态变量都复制到新部署的版本,因此升级变得昂贵。其中一些可能没有被使用并最终不必要地占用内存。
由于通用的存储模式,逻辑合约与代理合约紧密耦合 。因此,无法将这些逻辑合约与任何其他不继承通用存储合约的代理一起使用。这些问题可以通过其他存储模式有效解决。
永久存储:
这种方法的目标是最小化存储复制要求 ,如我们上一个示例所示。在这种方法中,一个单独的合约被维护为一个“永久的”存储合约 。
因此,所有的逻辑合约都使用这个永恒的存储合约来满足他们的存储需求。因此,从升级模式中删除了存储复制要求 。这种方法显着降低了升级成本。
EternalStorage.sol:
EternalStorage 合约维护映射作为它的状态变量。它还定义了访问这些映射的方法。
EternalStorage 合约是不可变的;因此,最好的方法是在部署此合约之前放置所有映射和访问器。这将允许逻辑合约在这个永久存储合约中存储和检索任何类型的应用程序数据。
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 pragma solidity ^0.8 .0 ; contract EternalStorage { mapping (bytes32 => address) _addressStorage; mapping (bytes32 => uint256) _uintStorage; mapping (bytes32 => string) _stringStorage; function getUint (bytes32 key ) public view returns (uint256) { return _uintStorage[key]; } function getAddress (bytes32 key ) public view returns (address) { return _addressStorage[key]; } function getString (bytes32 key ) public view returns (string memory) { return _stringStorage[key]; } function setUint (bytes32 key, uint256 value ) public { _uintStorage[key] = value; } function setAddress (bytes32 key, address value ) public { _addressStorage[key] = value; } function setString (bytes32 key, string memory value ) public { _stringStorage[key] = value; } }
EternalLogicLibrary.sol:
逻辑合约可以使用接口和库进一步划分。在此示例中,使用库合约从逻辑合约中抽象出逻辑。
库为代码抽象和可重用性提供了很好的案例 。
库类型合约不能有状态变量,不能持有以太币,也不能继承合约。将公共代码部署为库是经济的,因为合约大小会影响 gas 成本。以下库抽象了一些通用代码。
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 pragma solidity ^0.8 .0 ; import ".././EternalStorage.sol" ;library EternalLogicLibrary { function getUserAge (address _storageAddress ) external view returns (uint256 ){ return EternalStorage (_storageAddress).getUint ("age" ); } function getUserName (address _storageAddress ) external view returns (string memory ) { return EternalStorage (_storageAddress).getString ("name" ); } function getOwner (address eternalStorage ) external view returns (address ) { return EternalStorage (eternalStorage).getAddress ("owner" ); } function setUserAge (address _storageAddress, uint256 age ) external{ EternalStorage (_storageAddress).setUint ("age" , age); } function setUserName (address _storageAddress, string memory name ) external { EternalStorage (_storageAddress).setString ("name" , name); } }
EternalLogicV1.sol & EternalLogicV2.sol:
指定库后,是时候实现逻辑合约了。
EternalLogicV1 合约为用户的年龄和姓名提供访问器。EternalLogicV2合约升级了EternalLogicV1合约,增加了一个只允许合约拥有者设置属性的修饰符。
两个逻辑合约都使用 EternalLogicLibrary 作为地址类型。他们还指定永久存储地址来检索和修改用户的属性。请记住,永久存储在库内使用。
因此,永久存储合约与逻辑库挂钩。这样一来,永久的存储地址就知道了库中实现的所有方法
EternalLogicV1.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 pragma solidity ^0.8 .0 ; import "./libraries/EternalLogicLibrary.sol" ;contract EternalLogicV1 { using EternalLogicLibrary for address; address public _storage; constructor (address __storage ){ _storage = __storage; } function getUserAge ( ) external view returns (uint256 ){ return _storage.getUserAge (); } function getUserName ( ) external view returns (string memory ) { return _storage.getUserName (); } function setUserAge (uint256 age ) external{ _storage.setUserAge (age); } function setUserName (string memory name ) external { _storage.setUserName (name); } }
EternalLogicV2.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 pragma solidity ^0.8 .0 ; import "./libraries/EternalLogicLibrary.sol" ;contract EternalLogicV2 { using EternalLogicLibrary for address; address public _storage; constructor (address __storage ){ _storage = __storage; } modifier onlyOwner { require (msg.sender == _storage.getOwner (), "Only Owner" ); _; } function getUserAge ( ) external view returns (uint256 ){ return _storage.getUserAge (); } function getUserName ( ) external view returns (string memory ) { return _storage.getUserName (); } function setUserAge (uint256 age ) onlyOwner external{ _storage.setUserAge (age); } function setUserName (string memory name ) onlyOwner external { _storage.setUserName (name); } }
EternalProxy.sol:
最后,编写一个简单的代理来委托对逻辑合约的调用了。代理和永久合约是不可变的。
代理合约还与永久存储合约进行通信,以访问与代理相关的状态变量,如所有者和实现地址。以下是代理合约的示例:
EternalProxy.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 pragma solidity ^0.8 .0 ; import "./EternalStorage.sol" ;contract EternalProxy { EternalStorage _storage; constructor (EternalStorage __storage ){ _storage = __storage; _storage.setAddress ("owner" , msg.sender ); } function getImplementationAddress ( ) public view returns (address ){ return _storage.getAddress ("implementation" ); } function setImplementationAddress (address _implementation ) public { require (msg.sender ==_storage.getAddress ("owner" ), "Owner only" ); _storage.setAddress ("implementation" , _implementation); } function getOwnerAddress ( ) public view returns (address ) { return _storage.getAddress ("owner" ); } fallback () external{ address implementation = getImplementationAddress (); assembly { let ptr :=mload (0x40 ) calldatacopy (ptr, 0 , calldatasize ()) let response := delegatecall ( gas (), implementation, ptr, calldatasize (), 0 , 0 ) returndatacopy (ptr, 0 , returndatasize ()) switch response case 0 { revert (ptr, returndatasize ()) } default { return (ptr, returndatasize ()) } } } }
测试用例:
测试用例
测试结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Token contract test Proxy try to upgrade ImplementationAddress To EternalLogicV1.address [*]test_proxy_pattern_owner_is_correct [*]test_proxy_pattern_implementation_equals_to_logic_address [*]test_can_set_and_get_val_from_logicv1 [*]test_can_set_and_get_names_from_logicv1 [*]test_proxy_pattern_implementation_equals_to_new_logic_address Proxy try to upgrade ImplementationAddress To EternalLogicV2.address [*]test_can_set_and_get_val_from_logicv2 [*]test_can_set_and_get_names_from_logicv2 ✔ (2653ms) 1 passing (3s) ✨ Done in 7.09s.
缺点:
尽管这种模式非常具有成本效益,但这种方法存在一些问题。在编写 EternalStorage 合约时,必须确定所有基本变量类型。
永久存储的访问模式不是很简单。永久存储模式需要一个专用的存储合约,并且不适用于现有合约。
还有另一种流行的方法可以提供无缝升级体验。
非结构化存储:
在这种模式下,代理合约往往是具有一些基本功能的简约合约,并且大部分业务逻辑都在逻辑合约内部完成。
新的逻辑合约维护最新实现合约中存在的状态变量的顺序。
代理合约通常需要所有者和实现地址来维护所有权和版本控制。代理合约将这些属性指定为常量,并在合约的字节码中设置它们。OpenZepplin 将非结构化存储模式用于其可升级的合约服务。
下面是此模式的简单实现
UnstructuredLogicV1.sol:
UnstructuredLogicV1.sol
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pragma solidity ^0.8 .0 ; contract UnstructuredLogicV1 { uint256 val; function getVal ( ) external view returns (uint256 ) { return val; } function setVal (uint256 _newVal ) external { val = _newVal; } }
UnstructuredLogicV1 合约简单地为一个名为 val 的值提供访问器。
UnstructuredLogicV2.sol:
UnstructuredLogicV2.sol
UnstructuredLogicV2.sol 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pragma solidity ^0.8 .0 ; contract UnstructuredLogicV2 { uint256 val; uint256 newVal; function getVal ( ) external view returns (uint256) { return newVal; } function setVal (uint256 _newVal ) external { newVal = _newVal; } }
同样,UnstructuredLogicV2 合约通过添加一个名为 newVal 的新变量来升级 UnstructuredLogicV1 合约。
请注意,UnstructuredLogicV2 还包含在 UnstructuredLogicV1 合约中定义的状态变量。
UnstructuredProxy.sol:
UnstructuredProxy.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 pragma solidity ^0.8 .0 ; contract UnstructuredProxy { bytes32 private constant ownerPosition = bytes32 (uint256 ( keccak256 ('eip1967.proxy.owner' )) - 1 ); bytes32 private constant implementationPosition = bytes32 (uint256 ( keccak256 ('eip1967.proxy.implementation' )) - 1 ); function getImplementationAddress ( ) public view returns (address impl ){ bytes32 _implementationPosition = implementationPosition; assembly { impl := sload (_implementationPosition) } } function setImplementationAddress (address _implementationAddress ) public { bytes32 _implementationPosition = implementationPosition; assembly { sstore (_implementationPosition, _implementationAddress) } } function getOwnerAddress ( ) external view returns (address ownr ){ bytes32 _ownerPosition = ownerPosition; assembly { ownr := sload (_ownerPosition) } } function setOwnerAddress (address _ownerAddress ) external { bytes32 _ownerPosition = ownerPosition; assembly { sstore (_ownerPosition, _ownerAddress) } } fallback () external{ address implementation = getImplementationAddress (); assembly { let ptr :=mload (0x40 ) calldatacopy (ptr, 0 , calldatasize ()) let result := delegatecall ( gas (), implementation, ptr, calldatasize (), 0 , 0 ) returndatacopy (ptr, 0 , returndatasize ()) switch result case 0 { revert (ptr, returndatasize ()) } default { return (ptr, returndatasize ()) } } } }
即使解决了代理合约和逻辑合约之间的存储冲突问题,不同逻辑合约的状态变量之间仍然可能发生冲突。
这是因为在第一个逻辑合约中声明的变量占用了代理内部可用的第一个存储槽。请记住,程序将实现和所有权变量的值存储在相当随机的位置 。由于 UnstructuredProxy 的第一个存储槽被 val 变量占用,UnstructuredLogicV2 必须在声明上一个逻辑合约的变量后声明自己的状态变量。
1 2 3 4 5 6 7 8 ┌─────────────────────┬───────────────────┬──────────────┬────────┬───────────────────────────────────────┐ │ contract │ state_variable │ storage_slot │ offset │ type │ ├─────────────────────┼───────────────────┼──────────────┼────────┼───────────────────────────────────────┤ │ UnstructuredLogicV1 │ val │ 0 │ 0 │ t_uint256 │ │ UnstructuredLogicV2 │ val │ 0 │ 0 │ t_uint256 │ │ UnstructuredLogicV2 │ newVal │ 1 │ 0 │ t_uint256 │ └─────────────────────┴───────────────────┴──────────────┴────────┴───────────────────────────────────────┘
这种方法在代理存储内部保持严格的存储顺序,并有助于避免意外覆盖存储变量
测试用例:
测试用例
测试结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Token contract test Proxy try to upgrade ImplementationAddress To UnstructuredLogicV1.address [*]test_proxy_pattern_implementation_equals_to_logic_address Proxy setImplementationAddress successfully [*]test_can_set_and_get_val_from_logicv1 [*]test_proxy_pattern_implementation_equals_to_new_logic_address Proxy try to upgrade ImplementationAddress To UnstructuredLogicV2.address [*]test_can_set_and_get_names_from_logicv2 ✔ (1719ms) 1 passing (2s) ✨ Done in 5.73s.
Summary:
继承存储:
永久存储:
非结构化存储:
存储模式
改进点
特点
目的
缺点1
缺点2
缺点3
继承存储
逻辑和代理合约之间保持相同的存储顺序,从而避免状态冲突
逻辑合约中仍有部分状态变量
解决存储冲突问题
升级时,需要同步之前的状态变量,耗费gas
一些未使用的变量占用了内存
逻辑合约与代理合约紧密耦合,无法将这些逻辑合约与任何其他不继承通用存储合约的代理一起使用
永久存储
逻辑合约不存状态变量,状态变量都存在一个单独的“永久的”存储合约
所有的逻辑合约都使用这个永久存储合约来进行存储
最小化存储复制要求
在编写永久存储合约时,必须确定所有基本变量类型
永久存储的访问模式不是很简单
永久存储模式需要一个专用的存储合约,并且不适用于现有合约。
非结构化存储
不需要一个专用的存储合约
新的逻辑合约继续维护旧逻辑合约中存在的状态变量的顺序,代理合约通常需要所有者和实现地址来维护所有权和版本控制,代理合约将这些属性指定为常量,并在合约的字节码中设置它们。
——
——
——
——
具有继承、永久或非结构化存储的基于代理的可升级模式为智能合约提供了一个不错的可升级架构。
还有一些高级可升级模式。这些模式可以更好地控制可升级性,同时解决 Solidity 语言施加的一些基本限制。让我们在本系列的下一部分详细了解一些高级可升级模式
还不是完全理解,后续再反复看看
Refs: