Writing an upgradeable contract in solidity

image-20221028102022991

Why write upgradable contracts

Smart contracts in Ethereum are immutable by default. However, once the project party discovers contract loopholes in advance or wants to upgrade functions, the contract needs to be changeable, so it is important to write an upgradeable contract at the beginning. So we need to use upgradable contracts to enhance maintainability.

Upgrade contract overview

The upgrade contract is usually implemented using the proxy mode. There are two contracts in the working principle of this mode, one is the proxy contract, and the other is the implementation contract. The proxy contract is responsible for managing the contract status data, while the implementation contract is only responsible for executing the contract logic and does not store any state data. The user calls the proxy contract, and the proxy contract implements the contract delegate callto achieve the purpose of upgrading.

image-20221105184801563

At present, there are mainly 3 ways to replace/upgrade the implementation contract:

  • Diamond Implementation
  • Transparent Proxy Implementation
  • UUPS Implementation

At present, transparent proxy implementation and UUPS implementation are commonly used. The purpose is to replace the address of the implementation contract with a new one (upgraded contract). The transparent proxy method is to put the update implementation contract function in the proxy contract, and UUPS updatate to addressis Put the update implementation contract in the implementation contract.

transparent proxy

Transparent Proxy ( EIP1967 ) is an easy way to separate responsibilities between proxy contracts and contracts. In this case, upgradeTothe function is part of the proxy contract, and the implementing contract can upgradeTobe , changing where future function calls delegate.

However, there are some caveats. If the proxy contract and the implementation contract have a function with the same name and parameters , in the transparent proxy contract, this problem is handled by the proxy contract. The proxy contract msg.senderdetermines whether the user's call is executed in the proxy contract itself or in the implementation contract according to global variables. .

So if msg.senderit is the admin of the proxy, then the proxy will not delegate the call, if it is not the admin address, the proxy will delegate the call to the implementing contract, even if it matches one of the proxy's functions.

So there is this problem with transparent proxies: ownerthe address must be stored in memory, and using memory is one of the least efficient and expensive steps of interacting with a smart contract, every time a user calls the proxy, the proxy checks if the user is an administrator, This adds unnecessary gas costs to most transactions that occur. ( All in all, the cost is relatively high )

UUPS

UUPS Proxy ( EIP1822 ) is another way to separate responsibilities between proxy contracts and contracts. In the UUPS proxy mode, upgradeTothe function is part of the implementation contract and is used by the user through the proxy contract delegatecall.

In UUPS, whether it is an administrator or a user, all calls are sent to the implementation contract. The advantage of this is that every time we call, we don't have to access the storage space to check whether the user who started the call is an administrator, which improves efficiency and cost. TimelockIn addition, because it is to implement the contract, you can customize the function according to your needs, and add such as , etc. to each new implementation Access Control, which cannot be done in the transparent proxy.

The problem with the UUPS proxy is that upgradeTothe function exists in the implementation contract, which will add a lot of code and be easily attacked, and if the developer forgets to add this function, the contract will no longer be able to be upgraded.

Writing Upgradable Smart Contracts Using OpenZeppelin

Transparent Proxy in Action

  1. installation hardhatenvironment

    ## 安装升级包
    $ 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. Writing Upgradable Contracts

    // 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. department script

    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. Compile the contract & start the local node & deploy the contract on the local network

    $ yarn hardhat compile
    $ yarn hardhat node
    $ yarn hardhat run scripts/proxy/open_proxy/openProxy.ts --network localhost
    
    ## 部署完毕
    proxyAddress: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
    implementationAddress: 0x5FbDB2315678afecb367f032d93F642f64180aa3
    adminAddress: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
    

There are actually three contracts deployed:

  • agency contract
  • Realize the contract
  • ProxyAdmincontract

ProxyAdminThe contract is used to manage the proxy contract, including upgrading the contract and transferring contract ownership.

The steps to upgrade the contract are

  • Deploy a new implementation contract,
  • Call the upgrade-related methods in ProxyAdminthe contract to set a new implementation contract address.
  1. new implementation contract

    // 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. upgrade script

    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")
    }
    ...

    Execute the contract upgrade script as follows:

    0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0  original proxy address
    upgrade to OpenProxyV2...
    0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0  OpenProxyV2 address(should be the same)
    0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9  getImplementationAddress
    0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512  getAdminAddress

    It can be found that the address of the proxy contract and the address of the admin contract have not changed, only the address of the implementation contract has changed

The above contract upgrades.deployProxydeployed is the transparent proxy mode used by default. If you want to use UUPS proxy mode, you need to specify explicitly.


UUPS combat

The hardhat environment is still above, but there are two places that need to be changed:

  1. write contract

    // 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. Contract deployment script

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

    When compiling and deploying a contract in UUPS proxy mode, only two contracts will actually be deployed

    • agency contract
    • Realize the contract

    The steps to upgrade the contract at this time are

    • Deploy a new implementation contract,
    • Call the upgrade-related methods in ProxyAdminthe contract to set a new implementation contract address.

**************************
*****wx: mindcarver*******
*****公众号:区块链技术栈*****
**************************

reference

Article source code

https://eips.ethereum.org/EIPS/eip-1822

https://eips.ethereum.org/EIPS/eip-1967

https://blog.openzeppelin.com/the-state-of-smart-contract-upgrades/

https://blog.gnosis.pm/solidity-delegateproxy-contracts-e09957d0f201

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

https://docs.openzeppelin.com/upgrades-plugins/1.x/api-hardhat-upgrades

https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies#the-constructor-caveat

Guess you like

Origin blog.csdn.net/pulong0748/article/details/127707167