透明的可升级智能合约:带有代码说明的指南
#javascript #web3 #区块链 #smartcontract

智能合约彻底改变了区块链上交易的方式。他们是自我执行的合同,与买卖双方之间的协议条款直接写入代码行。一旦完成条款,合同就会自动执行交易而无需中介。

但是,当您需要在部署后需要升级合同时会发生什么?不幸的是,传统的智能合约是不可变的,这意味着部署后无法进行任何更新或更改。那是可升级的智能合约。

可升级的智能合约启用更新和更改,而不会丢失数据或合同地址。合同代码仍处于相同的地址,但实施已在幕后升级,并且合同与其存储的数据保持正常。

在本文中,我们将带您通过代码说明创建透明的可升级智能合约。我们将使用HardHat作为我们的开发环境,而Openzeppelin的透明代理作为我们升级的合同机制。

建立开发环境

在您首选的代码编辑器中创建一个名为“透明升级智能合约”的项目文件夹。打开您的编辑命令行并粘贴此命令以安装所有必要的依赖项:

yarn add --dev @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers ethers @nomiclabs/hardhat-etherscan @nomiclabs/hardhat-waffle chai ethereum-waffle hardhat hardhat-contract-sizer hardhat-deploy hardhat-gas-reporter prettier prettier-plugin-solidity solhint solidity-coverage dotenv

安装完成后,在合同文件夹中创建两个合同:Box.solBoxV2.sol

Box.sol在下面写下智能合约:

// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;

contract Box {
    uint256 internal value;

    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);

    // Stores a new value in the contract
    function store(uint256 newValue) public {
        value = newValue;
        emit ValueChanged(newValue);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }

    // returns the current version of the contract
    function version() public pure returns (uint256) {
        return 1;
    }
}

合同Box是一项简单的合同,可存储单个值。它具有三个功能:storeretrieveversion

  1. store函数在合同中存储一个新值,并发出以新值作为参数的事件。
  2. retrieve函数返回最后存储的值。
  3. version函数返回合同的当前版本,在这种情况下为1

BoxV2.sol中写下智能合约:

// contracts/BoxV2.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;

contract BoxV2 {
    uint256 internal value;

    // Emitted when the stored value changes
    event ValueChanged(uint256 newValue);

    // Stores a new value in the contract
    function store(uint256 newValue) public {
        value = newValue;
        emit ValueChanged(newValue);
    }

    // Reads the last stored value
    function retrieve() public view returns (uint256) {
        return value;
    }

    // Increments the stored value by 1
    function increment() public {
        value = value + 1;
        emit ValueChanged(value);
    }

    // returns the current version of the contract
    function version() public pure returns (uint256) {
        return 2;
    }
}

合同BoxV2存储一个UINT256值。它具有三个功能:

  1. store:此函数将UINT256值作为输入,并将其存储在合同中。它还以新值作为参数发出事件ValueChanged

  2. retrieve:此函数是返回最后存储值的视图函数。

  3. increment:此函数将存储的值增加1,并以新值作为参数发出事件ValueChanged

此外,合同具有函数version,该函数将合同的当前版本返回为UINT256。在这种情况下,该版本为2。此功能被标记为纯净,这意味着它不会修改合同的状态。

要部署智能合约,我们需要创建一个部署文件夹,该文件夹包含两个文件,即01-deploy-box.js02-deploy-boxV2.js

01-deploy-box.js中编写以下代码:

// deploy/01-deploy-box.js
const { developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS } = require("../helper-hardhat-config")

const { network } = require("hardhat")
const { verify } = require("../helper-functions")

module.exports = async ({ getNamedAccounts, deployments }) => {
    const { deploy, log } = deployments
    const { deployer } = await getNamedAccounts()

    const waitBlockConfirmations = developmentChains.includes(network.name)
        ? 1
        : VERIFICATION_BLOCK_CONFIRMATIONS

    log("----------------------------------------------------")

    const box = await deploy("Box", {
        from: deployer,
        args: [],
        log: true,
        waitConfirmations: waitBlockConfirmations,
        proxy: {
            proxyContract: "OpenZeppelinTransparentProxy",
            viaAdminContract: {
                name: "BoxProxyAdmin",
                artifact: "BoxProxyAdmin",
            },
        },
    })

    // Be sure to check out the hardhat-deploy examples to use UUPS proxies!
    // https://github.com/wighawag/template-ethereum-contracts

    // Verify the deployment
    if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
        log("Verifying...")
        const boxAddress = (await ethers.getContract("Box_Implementation")).address;
        await verify(boxAddress, [])
    }
    log("----------------------------------------------------")
}

module.exports.tags = ["all", "box"]

代码块包括以下内容:

  1. helper-hardhat-config file中导入诸如developmentChainsVERIFICATION_BLOCK_CONFIRMATIONS之类的辅助助手模块,来自hardhat的网络,并从helper-functions验证
  2. 定义将通过HardHat部署脚本调用的导出函数。该功能将进行两个参数,分别是getNamedAccounts和部署。
  3. 该功能使用部署对象部署Box智能合约。它从getNamedAccountsand检索Deployer帐户使用它作为合同的部署。
  4. 如果网络是一个开发链,则将waitBlockConfirmations变量设置为1,否则为VERIFICATION_BLOCK_CONFIRMATIONS
  5. 部署函数与Deployer帐户的合同名称Box的参数调用,没有对构造函数的论点,并且记录记录设置为true。此外,它指定了waitBlockConfirmations变量,并配有proxyContractviaAdminContract属性的代理对象。
  6. 然后通过调用verify函数来验证部署,但仅当网络不是开发链,并且设置了ETHERSCAN_API_KEY环境变量。
  7. 最后,该函数记录了一条消息,指示部署已完成。

in02-deploy-boxV2.js编写以下代码:

// deploy/02-deploy-box.js
const { developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS } = require("../helper-hardhat-config")

const { network } = require("hardhat")
const { verify } = require("../helper-functions")

module.exports = async ({ getNamedAccounts, deployments }) => {
    const { deploy, log } = deployments
    const { deployer } = await getNamedAccounts()

    const waitBlockConfirmations = developmentChains.includes(network.name)
        ? 1
        : VERIFICATION_BLOCK_CONFIRMATIONS

    log("----------------------------------------------------")

    const box = await deploy("BoxV2", {
        from: deployer,
        args: [],
        log: true,
        waitConfirmations: waitBlockConfirmations,
    })

    // Verify the deployment
    if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
        log("Verifying...")
        await verify(box.address, [])
    }
    log("----------------------------------------------------")
}

module.exports.tags = ["all", "boxv2"]

在上面的代码中,我们使用hardhat提供的部署和getNamedAccounts功能分别获取deployment信息并分别命名帐户。它还导入两个助手模块:helper-hardhat-confighelper-functions.

developmentChainsVERIFICATION_BLOCK_CONFIRMATIONShelper-hardhat-config模块所需的常数。

waitBlockConfirmations变量用于确定在考虑部署完成之前要等待多少个块确认。它设置为开发链的1和其他网络的VERIFICATION_BLOCK_CONFIRMATIONS

log函数用于将消息打印到控制台,指示部署过程的开始和结束。

部署了BoxV2合同后,代码将检查网络是否是开发网络,如果没有,则使用helper-functions模块使用verify函数验证合同部署。 verify该功能用于确保合同已正确部署并与源代码匹配。

最后,Module.exports.tags属性用于将标签添加到部署脚本。

这是01-deploy-box.js中的所有其他功能,02-deploy-boxV2.js

  1. verify来自helper-functions.js文件
const { run } = require("hardhat")

const verify = async (contractAddress, args) => {
    console.log("Verifying contract...")
    try {
        await run("verify:verify", {
            address: contractAddress,
            constructorArguments: args,
        })
    } catch (e) {
        if (e.message.toLowerCase().includes("already verified")) {
            console.log("Already verified!")
        } else {
            console.log(e)
        }
    }
}

module.exports = {
    verify,
}

verify使用硬汉库来验证在特定地址和特定构造方参数中部署的智能合约的bytecode

该函数记录了一条消息,指示验证过程已启动,然后从hardhat库中调用带有Verify:verify任务的run方法,并将contractAddressargs作为参数传递。

如果验证成功,则该功能将完成而无需任何进一步的操作。如果合同已经验证,则该函数将记录一条消息,表明已经对其进行了验证。否则,如果在验证过程中存在错误,则该函数将记录错误消息。

2. hardhat.config.js

require("@nomiclabs/hardhat-waffle")
require("@nomiclabs/hardhat-etherscan")
require("hardhat-deploy")
require("solidity-coverage")
require("hardhat-gas-reporter")
require("hardhat-contract-sizer")
require("dotenv").config()
require("@openzeppelin/hardhat-upgrades")

/**
 * @type import('hardhat/config').HardhatUserConfig
 */

const MAINNET_RPC_URL =
    process.env.MAINNET_RPC_URL ||
    process.env.ALCHEMY_MAINNET_RPC_URL ||
    "https://eth-mainnet.alchemyapi.io/v2/your-api-key"
const SEPOLIA_RPC_URL =
    process.env.SEPOLIA_RPC_URL || "https://eth-sepolia.g.alchemy.com/v2/YOUR-API-KEY"
const POLYGON_MAINNET_RPC_URL =
    process.env.POLYGON_MAINNET_RPC_URL || "https://polygon-mainnet.alchemyapi.io/v2/your-api-key"
const PRIVATE_KEY = process.env.PRIVATE_KEY
// optional
const MNEMONIC = process.env.MNEMONIC || "your mnemonic"

// Your API key for Etherscan, obtain one at https://etherscan.io/
const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || "Your etherscan API key"
const POLYGONSCAN_API_KEY = process.env.POLYGONSCAN_API_KEY || "Your polygonscan API key"
const REPORT_GAS = process.env.REPORT_GAS || false

module.exports = {
    defaultNetwork: "hardhat",
    networks: {
        hardhat: {
            // // If you want to do some forking, uncomment this
            // forking: {
            //   url: MAINNET_RPC_URL
            // }
            chainId: 31337,
        },
        localhost: {
            chainId: 31337,
        },
        sepolia: {
            url: SEPOLIA_RPC_URL,
            accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
            //   accounts: {
            //     mnemonic: MNEMONIC,
            //   },
            saveDeployments: true,
            chainId: 11155111,
        },
        mainnet: {
            url: MAINNET_RPC_URL,
            accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
            //   accounts: {
            //     mnemonic: MNEMONIC,
            //   },
            saveDeployments: true,
            chainId: 1,
        },
        polygon: {
            url: POLYGON_MAINNET_RPC_URL,
            accounts: PRIVATE_KEY !== undefined ? [PRIVATE_KEY] : [],
            saveDeployments: true,
            chainId: 137,
        },
    },
    etherscan: {
        // npx hardhat verify --network <NETWORK> <CONTRACT_ADDRESS> <CONSTRUCTOR_PARAMETERS>
        apiKey: {
            sepolia: ETHERSCAN_API_KEY,
            polygon: POLYGONSCAN_API_KEY,
        },
    },
    gasReporter: {
        enabled: REPORT_GAS,
        currency: "USD",
        outputFile: "gas-report.txt",
        noColors: true,
        // coinmarketcap: process.env.COINMARKETCAP_API_KEY,
    },
    contractSizer: {
        runOnCompile: false,
        only: ["Box"],
    },
    namedAccounts: {
        deployer: {
            default: 0, // here this will by default take the first account as deployer
            1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another
        },
        player: {
            default: 1,
        },
    },
    solidity: {
        compilers: [
            {
                version: "0.8.8",
            },
            {
                version: "0.4.24",
            },
        ],
    },
    mocha: {
        timeout: 200000, // 200 seconds max for running tests
    },
}

这是一个用于HardHat的配置文件,这是一个流行的以太坊智能合约的开发环境。配置文件指定了HardHat环境的各种设置和选项,包括网络配置,编译器版本,部署设置和测试选项。

此文件包括以下内容:

  1. 包括Hardhat WaffleHardhat Etherscan, Hardhat Deploy, Solidity CoverageHardhat Gas Reporter和Hardhat Contract的所需插件列表。
  2. 多个网络的配置设置,包括HardhatlocalSepoliaMainnetPolygon
  3. EtherscanPolygonscan apis的配置设置。
  4. 气体报告设置,包括已启用/禁用,货币,输出文件和CoinMarketCap API密钥。
  5. 命名帐户的配置设置,包括deployerplayer
  6. 要使用的固定编译器版本。
  7. Mocha测试设置,包括运行测试的200秒暂停。

总的来说,此配置文件用于设置硬窃环境,以高效有效的智能合同开发和测试。

3. helper-hardhat-config.js

const networkConfig = {
  default: {
    name: "hardhat",
  },
  31337: {
    name: "localhost",
  },
  11155111: {
    name: "sepolia",
  },
  1: {
    name: "mainnet",
  },
};

const developmentChains = ["hardhat", "localhost"];
const VERIFICATION_BLOCK_CONFIRMATIONS = 6;

module.exports = {
  networkConfig,
  developmentChains,
  VERIFICATION_BLOCK_CONFIRMATIONS,
};

上面的代码定义了导出三个变量的JavaScript模块:

  1. networkConfig:将网络ID映射到网络名称的对象。定义了四个网络:hardhat(默认),localhost(ID 31337),sepolia(ID 11155111)和Mainnet(ID 1)。
  2. developmentChains:一个包含用于开发目的的网络名称的数组。在这种情况下,它包括hardhatlocalhost
  3. VERIFICATION_BLOCK_CONFIRMATIONS:一个代表事务所需的块确认数量的常数。在这种情况下,将其设置为6。

总的来说,该代码定义了与网络ID,网络名称和块确认要求相关的一些配置设置,该要求可以由我们应用程序的其他部分使用。

4. .env文件

SEPOLIA_RPC_URL='https://eth-sepolia.g.alchemy.com/v2/YOUR-API-KEY'
POLYGON_MAINNET_RPC_URL='https://rpc-mainnet.maticvigil.com'
ALCHEMY_MAINNET_RPC_URL="https://eth-mainnet.alchemyapi.io/v2/your-api-key"
ETHERSCAN_API_KEY='YOUR_KEY'
POLYGONSCAN_API_KEY='YOUR_KEY'
PRIVATE_KEY='abcdefg'
MNEMONIC='abcdefsgshs'
REPORT_GAS=true
COINMARKETCAP_API_KEY="YOUR_KEY"
  1. SEPOLIA_RPC_URL:此环境变量指定Sepolia RPC端点的URL,该URL用于与以太坊区块链进行交互。该特定端点由炼金术提供,并且需要一个API键来访问它。
  2. POLYGON_MAINNET_RPC_URL:此环境变量指定Polygon Mainnet RPC端点的URL,该URL用于与多边形网络进行交互。此特定端点由MaticVigil提供,并且不需要API键来访问它。
  3. ALCHEMY_MAINNET_RPC_URL:此环境变量指定了炼金术主网RPC端点的URL,该端点也用于与以太坊区块链进行交互。该特定端点由炼金术提供,并且需要一个API键来访问它。
  4. ETHERSCAN_API_KEY:此环境变量指定Etherscan的API密钥,这是一个区块链资源管理器,可提供有关以太坊区块链的交易,地址和区块的信息。
  5. POLYGONSCAN_API_KEY:此环境变量指定了Polygonscan的API密钥,该浏览器是一个区块链资源管理器,可提供有关Polygon Network上的交易,地址和块的信息。
  6. PRIVATE_KEY:此环境变量指定特定以太坊帐户的私钥。此键用于签署交易并通过网络进行身份验证。
  7. MNEMONIC:此环境变量指定了一个可用于为以太坊帐户生成私钥的助记符。这是指定私钥的另一种方法,对于管理多个帐户可能很有用。
  8. REPORT_GAS:此环境变量指定是否报告每笔交易的气体成本。如果设置为真,将报告汽油成本。
  9. COINMARKETCAP_API_KEY:此环境变量指定CoinMarketCap的API密钥,该网站提供有关加密货币价格和市值的信息。

设置环境后,让S通过运行以下命令部署2个合同:

yarn hardhat deploy

要升级合同,让我们创建我们的脚本文件夹并创建upgrade-box.js,让S添加脚本以将我们的合同Box升级到BoxV2

upgrade-box.js中编写以下代码:

const { developmentChains, VERIFICATION_BLOCK_CONFIRMATIONS } = require("../helper-hardhat-config")
const { network, deployments, deployer } = require("hardhat")
const { verify } = require("../helper-functions")

async function main() {
    const { deploy, log } = deployments
    const { deployer } = await getNamedAccounts()

    const waitBlockConfirmations = developmentChains.includes(network.name)
        ? 1
        : VERIFICATION_BLOCK_CONFIRMATIONS

    log("----------------------------------------------------")

    const boxV2 = await deploy("BoxV2", {
        from: deployer,
        args: [],
        log: true,
        waitConfirmations: waitBlockConfirmations,
    })

    // Verify the deployment
    if (!developmentChains.includes(network.name) && process.env.ETHERSCAN_API_KEY) {
        log("Verifying...")
        await verify(boxV2.address, [])
    }

    // Upgrade!
    // Not "the hardhat-deploy way"
    const boxProxyAdmin = await ethers.getContract("BoxProxyAdmin")
    const transparentProxy = await ethers.getContract("Box_Proxy")
    const upgradeTx = await boxProxyAdmin.upgrade(transparentProxy.address, boxV2.address)
    await upgradeTx.wait(1)
    const proxyBox = await ethers.getContractAt("BoxV2", transparentProxy.address)
    const version = await proxyBox.version()
    console.log('New version : ',version.toString())
    log("----------------------------------------------------")
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error)
        process.exit(1)
    })

脚本执行以下步骤:

  1. 它从辅助 - hardhat-c​​onfig和helper-functions文件中导入一些必要的模块。
  2. 它定义了一个称为main的异步函数,该函数将在运行脚本时将执行。
  3. 该功能使用HardhatgetNamedAccounts函数获取命名帐户。
  4. 它决定了block confirmations的数量,以便在考虑部署成功之前等待。如果网络是一个开发网络,则将块确认的数量设置为1,否则,将其设置为helper-hardhat-config文件中定义的常数值。
  5. 它使用hardhat的deployments.deploy函数部署合同的新版本,传递了必要的论点,例如contractnamedeployeraddress以及block confirmations的数量等待。
  6. 如果网络不是开发网络,并且提供了Etherscan API key,则该脚本将尝试使用verify函数从helper-functions文件验证新合同的部署。
  7. 脚本然后升级现有合同实例以使用新版本。它首先使用Hardhat的ethers.getContract函数检索BoxProxyAdmin合同和现有Box_Proxy合同的合同实例。然后,它在BoxProxyAdmin合同上称为upgrade功能,并通过现有Box_Proxy合同的address和新部署的BoxV2合同的address。最后,它检索了升级的BoxV2合同的合同实例并记录其版本。
  8. 使用main()function在脚本的末尾调用该函数,并将任何错误记录到控制台。

通过运行以下命令来检查升级:

yarn hardhat node
yarn hardhat run scripts/upgrade-box.js --network localhost

输出将是:

New version : 2

总而言之,transparent upgradable smart contracts是区块链开发人员的强大工具。他们允许无缝升级可以部署合同而不破坏网络或要求用户切换到合同的新版本。在本文中,我们解释了transparent upgradable smart contracts的概念,并使用两个坚固合同及其部署脚本演示了实施示例。

有用的资源:
1.https://github.com/wighawag/template-ethereum-contracts
2.https://docs.openzeppelin.com/upgrades-plugins/1.x/
3.https://github.com/wighawag/template-ethereum-contracts/tree/examples/openzeppelin-proxies/deploy