安全地部署和升级智能合约

Defender 允许你轻松地跨链部署和升级智能合约,同时保持最佳安全实践。本教程展示了如何使用 Relayer 部署一个名为 Box 的合约,并通过 Safe 钱包 (多重签名) 使用 UUPS 代理模式对其进行升级。

前提条件

  • OpenZeppelin Defender 账户。

  • NodeJS 和 NPM

  • 任何 IDE 或文本编辑器

  • 带有 Metamask (或任何其他兼容钱包) 的 Web 浏览器,并已充值 Sepolia ETH。

1. 配置

Safe 钱包

首先,你需要创建一个 Safe 钱包来管理升级过程。为此,请按照以下步骤操作:

  1. 在 Web 浏览器中打开 Safe app 并连接你的钱包 (确保你已连接到 Sepolia 测试网)。

  2. 点击 创建新账户 并按照步骤操作。

  3. 记下你创建的 Safe 钱包的地址,稍后你需要它。

    部署 safe 复制地址

环境设置

现在,你将使用 Sepolia 测试网创建一个 Defender 测试环境,你将在其中部署和升级智能合约。为此,请按照以下步骤操作:

  1. 打开 Defender Deploy

  2. 点击 设置

    部署环境页面
  3. 从下拉列表中选择 Sepolia

    部署网络向导
  4. 选择与你已充值的 relayer 关联的审批流程,该流程将为测试环境执行部署。如果你还没有审批流程,Defender 将允许你在向导流程中创建一个。Relayer 自动执行 Gas 费用支付,并负责私钥安全存储、交易签名、nonce 管理、Gas 定价估计和重新提交。但是,你也可以选择使用 EOA (“外部拥有账户”) 或 Safe 钱包进行部署。

    阅读更多关于 relayer 以及如何管理它们的信息 here

    部署 block 部署向导
  5. 点击审批流程字段以展开下拉列表,然后点击 创建审批流程。输入 "Safe Wallet Approval Process" 作为名称,然后展开合约字段以点击 添加合约。输入 "Safe Wallet" 作为名称,粘贴你之前复制的 Safe 钱包的地址,然后点击 创建。在合约下拉列表中选择 "Safe Wallet",然后点击 继续

    部署 block 升级向导
  6. Defender 将为此环境生成 API 密钥和密钥,因此请安全地复制并存储它们。点击 开始部署 以访问环境页面。

    部署 block 结束向导

你配置了测试环境以便在不损失实际资金风险的情况下进行学习。设置生产环境的步骤相同。

Defender 支持 Hardhat 和 Foundry 集成。选择适合你项目的那个!

Foundry 设置

首先,确保你已安装 Foundry。按照以下步骤创建一个新目录和项目:

  1. 在终端中运行以下命令:

    forge init deploy-tutorial && cd deploy-tutorial && forge install foundry-rs/forge-std && forge install OpenZeppelin/openzeppelin-foundry-upgrades && forge install OpenZeppelin/openzeppelin-contracts-upgradeable
  2. 现在,配置 foundry.toml 文件以启用 ffi, ast, build info 和 storage layout:

    [profile.default]
    ffi = true
    ast = true
    build_info = true
    extra_output = ["storageLayout"]
  3. 在项目根目录中创建一个名为 .env 的新文件,并添加以下内容,其中包含你在创建 Defender 环境后收到的密钥:

    DEFENDER_KEY = "<<YOUR_KEY>>"
    DEFENDER_SECRET = "<<YOUR_SECRET>>"

Hardhat 设置

首先,确保你已安装带有 ethers v6 的 Hardhat。按照以下步骤创建一个新目录和项目:

  1. 在终端中运行以下命令:

    mkdir deploy-tutorial && cd deploy-tutorial && npx hardhat init
  2. Hardhat 将询问一些问题来设置配置,所以回答以下问题:

    • 你想做什么:创建一个 Typescript 项目

    • Hardhat 项目根目录:保持原样

    • 你想使用 .gitignore 吗: 是

    • 你想使用 npm 安装此示例项目的依赖项吗:是

  3. Hardhat 现在将安装工具库,并为你创建项目文件。之后,使用以下命令安装 OpenZeppelin 包:

    npm i @openzeppelin/hardhat-upgrades @openzeppelin/contracts-upgradeable dotenv --save-dev

    安装完成后,你的初始目录结构应如下所示:

    部署目录结构
  4. 现在你需要编辑你的 Hardhat 配置来添加 Defender 密钥和 Sepolia 网络。打开 hardhat.config.ts 文件,并将其内容替换为以下代码:

    import { HardhatUserConfig } from "hardhat/config";
    import "@nomicfoundation/hardhat-toolbox";
    import "@openzeppelin/hardhat-upgrades";
    
    require("dotenv").config();
    
    const config: HardhatUserConfig = {
      solidity: "0.8.20",
      defender: {
        apiKey: process.env.DEFENDER_KEY as string,
        apiSecret: process.env.DEFENDER_SECRET as string,
      },
      networks: {
        sepolia: {
          url: "https://ethereum-sepolia.publicnode.com",
          chainId: 11155111
        },
      },
    };
    
    export default config;
  5. 在项目根目录中创建一个名为 .env 的新文件,并添加以下内容,其中包含你在创建 Defender 环境后收到的密钥:

    DEFENDER_KEY = "<<YOUR_KEY>>"
    DEFENDER_SECRET = "<<YOUR_SECRET>>"

2. 部署

  1. contractssrc 目录中创建一个名为 Box.sol 的新文件,并添加以下代码:

    // SPDX-License-Identifier: Unlicense
    pragma solidity ^0.8.20;
    
    import {Initializable} from  "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
    import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
    import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
    
    /// @title Box
    /// @notice A box with objects inside.
    contract Box is Initializable, UUPSUpgradeable, OwnableUpgradeable {
        /*//////////////////////////////////////////////////////////////
                                    VARIABLES
        //////////////////////////////////////////////////////////////*/
    
        /// @notice Number of objects inside the box.
        uint256 public numberOfObjects;
    
        /*//////////////////////////////////////////////////////////////
                                    FUNCTIONS
        //////////////////////////////////////////////////////////////*/
    
        /// @notice No constructor in upgradable contracts, so initialized with this function.
        function initialize(uint256 objects, address multisig) public initializer {
            __UUPSUpgradeable_init();
            __Ownable_init(multisig);
    
            numberOfObjects = objects;
        }
    
        /// @notice Remove an object from the box.
        function removeObject() external {
            require(numberOfObjects > 1, "Nothing inside");
            numberOfObjects -= 1;
        }
    
        /// @dev Upgrades the implementation of the proxy to new address.
        function _authorizeUpgrade(address) internal override onlyOwner {}
    }

    这是一个复制一个 box 的合约,具有三个功能:

    • initialize(): 使用其初始实现初始化可升级代理,并将多重签名设置为所有者。

    • removeObject(): 通过删除一个来减少 box 中的对象数量。

    • _authorizeUpgrade(): 将代理指向一个新的实现地址。

Foundry

  1. script 目录中创建一个名为 Deploy.s.sol 的文件。此脚本将通过 Defender 部署可升级的 Box 合约,其中包含 5 个初始对象,并将所有者设置为在环境设置中配置的多重签名地址。initializer 选项用于在合约部署后调用 initialize() 函数。将以下代码复制并粘贴到 Deploy.s.sol 中:

    // SPDX-License-Identifier: Unlicense
    pragma solidity ^0.8.20;
    
    import {Script} from "forge-std/Script.sol";
    import {console} from "forge-std/console.sol";
    
    import {Defender, ApprovalProcessResponse} from "openzeppelin-foundry-upgrades/Defender.sol";
    import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
    
    import {Box} from "src/Box.sol";
    
    contract DefenderScript is Script {
        function setUp() public {}
    
        function run() public {
            ApprovalProcessResponse memory upgradeApprovalProcess = Defender.getUpgradeApprovalProcess();
    
            if (upgradeApprovalProcess.via == address(0)) {
                revert(
                    string.concat(
                        "Upgrade approval process with id ",
                        upgradeApprovalProcess.approvalProcessId,
                        " has no assigned address"
                    )
                );
            }
    
            Options memory opts;
            opts.defender.useDefenderDeploy = true;
    
            address proxy =
                Upgrades.deployUUPSProxy("Box.sol", abi.encodeCall(Box.initialize, (5, upgradeApprovalProcess.via)), opts);
    
            console.log("Deployed proxy to address", proxy);
        }
    }
  2. 通过运行以下命令部署,该命令执行你的部署脚本:

    forge script script/Deploy.s.sol --force --rpc-url https://ethereum-sepolia.publicnode.com

Hardhat

  1. 打开 scripts 目录中的 deploy.ts 文件。此脚本将通过 Defender 部署可升级的 Box 合约,其中包含 5 个初始对象,并将所有者设置为在环境设置中配置的多重签名地址。initializer 选项用于在合约部署后调用 initialize() 函数。将以下代码复制并粘贴到 deploy.ts 中:

    import { ethers, defender } from "hardhat";
    
    async function main() {
      const Box = await ethers.getContractFactory("Box");
    
      const upgradeApprovalProcess = await defender.getUpgradeApprovalProcess();
    
      if (upgradeApprovalProcess.address === undefined) {
        throw new Error(`Upgrade approval process with id ${upgradeApprovalProcess.approvalProcessId} has no assigned address`);
      }
    
      const deployment = await defender.deployProxy(Box, [5, upgradeApprovalProcess.address], { initializer: "initialize" });
    
      await deployment.waitForDeployment();
    
      console.log(`Contract deployed to ${await deployment.getAddress()}`);
    }
    
    // We recommend this pattern to be able to use async/await everywhere
    // and properly handle errors.
    main().catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });

    对于可升级合约,你应该使用 deployProxy()deployBeacon()deployImplementation(),对于不可升级合约,应该使用 deployContract()。要强制使用 deployContract(),请将 unsafeAllowDeployContract 选项设置为 true。更多信息请参考 here

  2. 通过运行以下命令部署你的 box,该命令执行你的部署脚本:

    npx hardhat run --network sepolia scripts/deploy.ts

成功! 你的合约应该已部署在 Sepolia 测试网上。导航到 Defender 中的部署并检查代理和实现是否已部署在测试环境中。所有 Box 交易都应发送到代理地址,因为它将存储状态并指向给定的实现。复制代理的地址以便稍后升级它。

已部署的合约

警告

默认情况下,Defender 利用 CREATE 操作码来部署合约。此方法创建一个新的合约实例,并为其分配一个唯一的地址。此地址由交易的 nonce 和发送者的地址决定。

Defender 还提供使用 CREATE2 操作码的高级部署选项。当部署请求包含 salt 时,Defender 会切换为使用 CREATE2 操作码。此操作码允许你根据发送者的 addresssalt 和合约 bytecode 的组合将合约部署到确定性地址。

虽然 CREATE2 提供了确定性的合约地址,但它会改变 msg.sender 的行为。在 CREATE2 部署中,构造函数或初始化代码中的 msg.sender 指的是工厂地址,而不是标准 CREATE 部署中的部署地址。这种区别会影响合约逻辑,因此在选择 CREATE2 时,建议进行仔细的测试和考虑。

3. 升级

升级智能合约允许更改其逻辑,同时保持相同的地址和存储。

  1. contractssrc 目录中创建一个名为 BoxV2.sol 的文件,并添加以下代码:

    // SPDX-License-Identifier: Unlicense
    pragma solidity ^0.8.20;
    
    import {Box} from "./Box.sol";
    
    /// @title BoxV2
    /// @notice An improved box with objects inside.
    /// @custom:oz-upgrades-from Box
    contract BoxV2 is Box {
        /*//////////////////////////////////////////////////////////////
                                    FUNCTIONS
        //////////////////////////////////////////////////////////////*/
    
        /// @notice Add an object to the box.
        function addObject() external {
            numberOfObjects += 1;
        }
    
        /// @notice Returns the box version.
        function boxVersion() external pure returns (uint256) {
            return 2;
        }
    }

    这是一个向你的 box 添加两个新功能的合约:

    • addObject(): 通过添加一个来增加 box 中的对象数量。

    • boxVersion(): 返回 box 实现的版本。

Foundry

  1. script 目录中创建一个名为 Upgrade.s.sol 的文件并粘贴以下代码。确保将 <PROXY ADDRESS> 替换为你之前复制的代理的地址。

    // SPDX-License-Identifier: Unlicense
    pragma solidity ^0.8.20;
    
    import {Script} from "forge-std/Script.sol";
    import {console} from "forge-std/console.sol";
    
    import {ProposeUpgradeResponse, Defender, Options} from "openzeppelin-foundry-upgrades/Defender.sol";
    
    contract DefenderScript is Script {
        function setUp() public {}
    
        function run() public {
            Options memory opts;
            ProposeUpgradeResponse memory response = Defender.proposeUpgrade(
                <PROXY ADDRESS>,
                "BoxV2.sol",
                opts
            );
            console.log("Proposal id", response.proposalId);
            console.log("Url", response.url);
        }
    }
  2. 使用以下命令使用升级脚本创建升级提案:

    forge script script/Upgrade.s.sol --force --rpc-url https://ethereum-sepolia.publicnode.com

Hardhat

  1. scripts 目录中创建一个名为 upgrade.ts 的文件并粘贴以下代码。确保将 <PROXY ADDRESS> 替换为你之前复制的代理的地址。

    import { ethers, defender } from "hardhat";
    
    async function main() {
      const BoxV2 = await ethers.getContractFactory("BoxV2");
    
      const proposal = await defender.proposeUpgradeWithApproval('<PROXY ADDRESS>', BoxV2);
    
      console.log(`Upgrade proposed with URL: ${proposal.url}`);
    }
    
    // We recommend this pattern to be able to use async/await everywhere
    // and properly handle errors.
    main().catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });
  2. 使用以下命令使用升级脚本创建升级提案:

    npx hardhat run --network sepolia scripts/upgrade.ts

批准

  1. 导航到 Defender 测试环境 并点击升级提案,这会在屏幕右侧展开一个模态框。

  2. 点击 查看交易提案,然后点击页面右上角的 批准并执行。使用你用于创建 Safe 钱包的钱包签署并执行交易。

你的 box 现在应该已升级到新版本! 你测试环境页面中的升级提案现在应标记为 已执行

已执行的升级提案

下一步

恭喜! 你现在可以使用相同的环境部署和升级其他合约。如果你对高级用例感兴趣,我们正在编写与部署相关的指南。

部署合约后,我们建议使用 Defender 监控其状态和交易。了解如何使用 Monitor here