乐趣区

关于以太坊:死磕solidity之编写可升级合约

为什么要编写可降级合约

默认状况下,以太坊中的智能合约是不可变的。然而一旦我的项目方提前发现合约破绽或者想降级性能,是须要合约能够变动的,因而一开始编写可降级的合约是重要的。因而咱们须要应用可降级的合约来加强可维护性。

降级合约概述

降级合约通常是采纳代理模式来实现,这种模式的工作原理存在两个合约,一个是代理合约,一个是实现合约,代理合约负责管理合约状态数据,而实现合约只是负责执行合约逻辑,不存储任何状态数据。用户通过调用代理合约,代理合约对实现合约进行 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 中,不论是管理员还是用户,所有的调用都被发送到实现合约中。这样做的益处是,每次调用时,咱们不用拜访存储空间来查看开始调用的用户是否是管理员,这进步了效率和老本。另外,因为是实现合约,你能够依据你的须要定制性能,在每一个新的实现中退出诸如 TimelockAccess Control 等,这在通明代理中是做不到的。

UUPS 代理存在的问题是:upgradeTo函数存在于实现合约中,会减少不少代码,容易被攻打,并且如果开发者遗记增加这个函数,合约将不能再降级了。

应用 OpenZeppelin 编写可降级智能合约

通明代理实战

  1. 装置 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
  2. 编写可降级合约

    // 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;}
    }
    
  3. 部署脚本

    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
    })
    
  4. 编译合约 & 启动本地节点 & 本地网络部署合约

    $ 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 合约中降级相干的办法,设置新的实现合约地址。
  1. 新的实现合约

    // 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;}
    }
  2. 降级脚本

    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 环境还是以上,只不过有两个中央须要更改:

  1. 编写合约

    // 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];
        }
    }
    
  2. 合约部署脚本

    ## 只须要略微变动一下
    // 部署合约, 并调用初始化办法
    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…

退出移动版