为什么要编写可降级合约
默认状况下,以太坊中的智能合约是不可变的。然而一旦我的项目方提前发现合约破绽或者想降级性能,是须要合约能够变动的,因而一开始编写可降级的合约是重要的。因而咱们须要应用可降级的合约来加强可维护性。
降级合约概述
降级合约通常是采纳代理模式来实现,这种模式的工作原理存在两个合约,一个是代理合约,一个是实现合约,代理合约负责管理合约状态数据,而实现合约只是负责执行合约逻辑,不存储任何状态数据。用户通过调用代理合约,代理合约对实现合约进行 delegate call
从而达到降级的目标。
目前次要有 3 种形式来替换 / 降级实现合约:
- Diamond Implementation
- Transparent Proxy Implementation
- UUPS Implementation
目前通用的是通明代理实现和 UUPS 实现,目标都是为了将实现合约的地址换成新的(降级后的合约),通明代理的形式是把更新实现合约函数 updatate to address
放在代理合约里,而 UUPS 是把更新实现合约放在实现合约中。
通明代理
通明代理(EIP1967)是一种简略的办法来拆散代理合约和合约之间的责任。在这种状况下,upgradeTo
函数是代理合约中的一部分,而实现合约能够通过在代理上调用 upgradeTo
来降级,从而扭转将来函数调用的委托地位。
不过也有一些注意事项。代理合约和实现合约如果有一个 名称和参数雷同的函数 ,在通明代理合约中,这个问题由代理合约来解决,代理合约依据msg.sender
全局变量来决定用户的调用是在代理合约自身还是在实现合约中执行。
所以如果 msg.sender
是代理的管理员,那么代理将不会委托调用,如果它不是管理员地址,代理将把调用委托给实现合约,即便它与代理的某个函数相匹配。
所以通明代理存在此问题:owner
的地址必须存储在存储器中,而应用存储器是与智能合约互动的最低效和最低廉的步骤之一,每次用户调用代理时,代理会检查用户是否是管理员,这给大多数产生的交易减少了不必要的气体老本。(总而言之老本比拟高)
UUPS
UUPS 代理(EIP1822)是另一种办法来拆散代理合约和合约之间的责任。UUPS 代理模式下,upgradeTo
函数是实现合约的一部分,并且通过代理合约被用户应用delegatecall
。
在 UUPS 中,不论是管理员还是用户,所有的调用都被发送到实现合约中。这样做的益处是,每次调用时,咱们不用拜访存储空间来查看开始调用的用户是否是管理员,这进步了效率和老本。另外,因为是实现合约,你能够依据你的须要定制性能,在每一个新的实现中退出诸如 Timelock
、Access Control
等,这在通明代理中是做不到的。
UUPS 代理存在的问题是:upgradeTo
函数存在于实现合约中,会减少不少代码,容易被攻打,并且如果开发者遗记增加这个函数,合约将不能再降级了。
应用 OpenZeppelin 编写可降级智能合约
通明代理实战
-
装置
hardhat
环境## 装置升级包 $ yarn add @openzeppelin/contracts @openzeppelin/contracts-upgradeable @openzeppelin/hardhat-upgrades ## 配置文件 import {HardhatUserConfig} from 'hardhat/config' import '@nomicfoundation/hardhat-toolbox' import '@openzeppelin/hardhat-upgrades' const config: HardhatUserConfig = {solidity: '0.8.17'} export default config
-
编写可降级合约
// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract OpenProxy is Initializable { uint public value; function initialize(uint _value) public initializer {value = _value;} function increaseValue() external {++value;} }
-
部署脚本
import {ethers, upgrades} from 'hardhat' // yarn hardhat run scripts/deploy_openProxy.ts --network localhost async function main() {const OpenProxy = await ethers.getContractFactory('OpenProxy') // 部署合约, 并调用初始化办法 const myOpenProxy = await upgrades.deployProxy(OpenProxy, [10], {initializer: 'initialize'}) // 代理合约地址 const proxyAddress = myOpenProxy.address // 实现合约地址 const implementationAddress = await upgrades.erc1967.getImplementationAddress(myOpenProxy.address) // proxyAdmin 合约地址 const adminAddress = await upgrades.erc1967.getAdminAddress(myOpenProxy.address) console.log(`proxyAddress: ${proxyAddress}`) console.log(`implementationAddress: ${implementationAddress}`) console.log(`adminAddress: ${adminAddress}`) } main().catch((error) => {console.error(error) process.exitCode = 1 })
-
编译合约 & 启动本地节点 & 本地网络部署合约
$ yarn hardhat compile $ yarn hardhat node $ yarn hardhat run scripts/proxy/open_proxy/openProxy.ts --network localhost ## 部署结束 proxyAddress: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 implementationAddress: 0x5FbDB2315678afecb367f032d93F642f64180aa3 adminAddress: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
实际上部署的合约有三个:
- 代理合约
- 实现合约
ProxyAdmin
合约
ProxyAdmin
合约是用来治理代理合约的,包含了降级合约,转移合约所有权。
降级合约的步骤就是
- 部署一个新的实现合约,
- 调用
ProxyAdmin
合约中降级相干的办法,设置新的实现合约地址。
-
新的实现合约
// SPDX-License-Identifier: MIT pragma solidity ^0.8.9; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; contract OpenProxyV2 is Initializable { uint public value; function initialize(uint _value) public initializer {value = _value;} function increaseValue() external {--value;} }
-
降级脚本
import {ethers} from "hardhat"; import {upgrades} from "hardhat"; const proxyAddress = '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' async function main() {console.log(proxyAddress, "original proxy address") const OpenProxyV2 = await ethers.getContractFactory("OpenProxyV2") console.log("upgrade to OpenProxyV2...") const myOpenProxyV2 = await upgrades.upgradeProxy(proxyAddress, OpenProxyV2) console.log(myOpenProxyV2.address, "OpenProxyV2 address(should be the same)") console.log(await upgrades.erc1967.getImplementationAddress(myOpenProxyV2.address), "getImplementationAddress") console.log(await upgrades.erc1967.getAdminAddress(myOpenProxyV2.address), "getAdminAddress") } ...
执行合约降级脚本如下:
0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 original proxy address upgrade to OpenProxyV2... 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 OpenProxyV2 address(should be the same) 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9 getImplementationAddress 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 getAdminAddress
能够发现代理合约地址和 admin 合约的地址并没有扭转,仅仅是实现合约的地址产生了变动
以上通过 upgrades.deployProxy
部署的合约,默认状况下是应用的通明代理模式。如果你要想应用 UUPS 代理模式,须要显示的指定。
UUPS 实战
hardhat 环境还是以上,只不过有两个中央须要更改:
-
编写合约
// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; contract LogicV1 is Initializable, UUPSUpgradeable, OwnableUpgradeable {function initialize() public initializer {__Ownable_init(); __UUPSUpgradeable_init();} /// @custom:oz-upgrades-unsafe-allow constructor constructor() initializer {} // 须要此办法来避免未经受权的降级,因为在 UUPS 模式中,降级是从实现合约实现的,而在通明代理模式中,降级是通过代理合约实现的 function _authorizeUpgrade(address) internal override onlyOwner {} mapping(string => uint256) private logic; event logicSetted(string indexed _key, uint256 _value); function SetLogic(string memory _key, uint256 _value) external {logic[_key] = _value; emit logicSetted(_key, _value); } function GetLogic(string memory _key) public view returns (uint256) {return logic[_key]; } }
-
合约部署脚本
## 只须要略微变动一下 // 部署合约, 并调用初始化办法 const myLogicV1 = await upgrades.deployProxy(LogicV1, { initializer: 'initialize', kind: 'uups' })
Warning: A proxy admin was previously deployed on this network // 管理员合约理论不存在了,只有代理合约和实现合约 proxyAddress: 0xa513E6E4b8f2a923D98304ec87F64353C4D5C853 implementationAddress: 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 adminAddress: 0x0000000000000000000000000000000000000000
编译并部署 UUPS 代理模式的合约时,理论只会部署两个合约
- 代理合约
- 实现合约
此时的降级合约的步骤就是
- 部署一个新的实现合约,
- 调用
ProxyAdmin
合约中降级相干的办法,设置新的实现合约地址。
**************************
*****wx: mindcarver*******
***** 公众号: 区块链技术栈 *****
**************************
参考
文章源码
https://eips.ethereum.org/EIP…
https://eips.ethereum.org/EIP…
https://blog.openzeppelin.com…
https://blog.gnosis.pm/solidi…
https://blog.openzeppelin.com…
https://docs.openzeppelin.com…
https://docs.openzeppelin.com…