安全地部署和升级智能合约
1. 配置
Safe 钱包
首先,你需要创建一个 Safe 钱包来管理升级过程。为此,请按照以下步骤操作:
-
在 Web 浏览器中打开 Safe app 并连接你的钱包 (确保你已连接到 Sepolia 测试网)。
-
点击 创建新账户 并按照步骤操作。
-
记下你创建的 Safe 钱包的地址,稍后你需要它。
环境设置
现在,你将使用 Sepolia 测试网创建一个 Defender 测试环境,你将在其中部署和升级智能合约。为此,请按照以下步骤操作:
-
打开 Defender Deploy。
-
点击 设置。
-
从下拉列表中选择 Sepolia。
-
选择与你已充值的 relayer 关联的审批流程,该流程将为测试环境执行部署。如果你还没有审批流程,Defender 将允许你在向导流程中创建一个。Relayer 自动执行 Gas 费用支付,并负责私钥安全存储、交易签名、nonce 管理、Gas 定价估计和重新提交。但是,你也可以选择使用 EOA (“外部拥有账户”) 或 Safe 钱包进行部署。
阅读更多关于 relayer 以及如何管理它们的信息 here。
-
点击审批流程字段以展开下拉列表,然后点击 创建审批流程。输入 "Safe Wallet Approval Process" 作为名称,然后展开合约字段以点击 添加合约。输入 "Safe Wallet" 作为名称,粘贴你之前复制的 Safe 钱包的地址,然后点击 创建。在合约下拉列表中选择 "Safe Wallet",然后点击 继续。
-
Defender 将为此环境生成 API 密钥和密钥,因此请安全地复制并存储它们。点击 开始部署 以访问环境页面。
你配置了测试环境以便在不损失实际资金风险的情况下进行学习。设置生产环境的步骤相同。 |
Defender 支持 Hardhat 和 Foundry 集成。选择适合你项目的那个!
Foundry 设置
首先,确保你已安装 Foundry。按照以下步骤创建一个新目录和项目:
-
在终端中运行以下命令:
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
-
现在,配置
foundry.toml
文件以启用 ffi, ast, build info 和 storage layout:[profile.default] ffi = true ast = true build_info = true extra_output = ["storageLayout"]
-
在项目根目录中创建一个名为
.env
的新文件,并添加以下内容,其中包含你在创建 Defender 环境后收到的密钥:DEFENDER_KEY = "<<YOUR_KEY>>" DEFENDER_SECRET = "<<YOUR_SECRET>>"
Hardhat 设置
首先,确保你已安装带有 ethers v6 的 Hardhat。按照以下步骤创建一个新目录和项目:
-
在终端中运行以下命令:
mkdir deploy-tutorial && cd deploy-tutorial && npx hardhat init
-
Hardhat 将询问一些问题来设置配置,所以回答以下问题:
-
你想做什么:创建一个 Typescript 项目
-
Hardhat 项目根目录:保持原样
-
你想使用 .gitignore 吗: 是
-
你想使用 npm 安装此示例项目的依赖项吗:是
-
-
Hardhat 现在将安装工具库,并为你创建项目文件。之后,使用以下命令安装 OpenZeppelin 包:
npm i @openzeppelin/hardhat-upgrades @openzeppelin/contracts-upgradeable dotenv --save-dev
安装完成后,你的初始目录结构应如下所示:
-
现在你需要编辑你的 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;
-
在项目根目录中创建一个名为
.env
的新文件,并添加以下内容,其中包含你在创建 Defender 环境后收到的密钥:DEFENDER_KEY = "<<YOUR_KEY>>" DEFENDER_SECRET = "<<YOUR_SECRET>>"
2. 部署
-
在
contracts
或src
目录中创建一个名为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
-
在
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); } }
-
通过运行以下命令部署,该命令执行你的部署脚本:
forge script script/Deploy.s.sol --force --rpc-url https://ethereum-sepolia.publicnode.com
Hardhat
-
打开
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。 -
通过运行以下命令部署你的 box,该命令执行你的部署脚本:
npx hardhat run --network sepolia scripts/deploy.ts
成功! 你的合约应该已部署在 Sepolia 测试网上。导航到 Defender 中的部署并检查代理和实现是否已部署在测试环境中。所有 Box 交易都应发送到代理地址,因为它将存储状态并指向给定的实现。复制代理的地址以便稍后升级它。

警告
默认情况下,Defender 利用 CREATE
操作码来部署合约。此方法创建一个新的合约实例,并为其分配一个唯一的地址。此地址由交易的 nonce 和发送者的地址决定。
Defender 还提供使用 CREATE2
操作码的高级部署选项。当部署请求包含 salt
时,Defender 会切换为使用 CREATE2
操作码。此操作码允许你根据发送者的 address
、salt
和合约 bytecode
的组合将合约部署到确定性地址。
虽然 |
3. 升级
升级智能合约允许更改其逻辑,同时保持相同的地址和存储。
-
在
contracts
或src
目录中创建一个名为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
-
在
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); } }
-
使用以下命令使用升级脚本创建升级提案:
forge script script/Upgrade.s.sol --force --rpc-url https://ethereum-sepolia.publicnode.com
Hardhat
-
在
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; });
-
使用以下命令使用升级脚本创建升级提案:
npx hardhat run --network sepolia scripts/upgrade.ts
批准
-
导航到 Defender 测试环境 并点击升级提案,这会在屏幕右侧展开一个模态框。
-
点击 查看交易提案,然后点击页面右上角的 批准并执行。使用你用于创建 Safe 钱包的钱包签署并执行交易。
你的 box 现在应该已升级到新版本! 你测试环境页面中的升级提案现在应标记为 已执行。

下一步
恭喜! 你现在可以使用相同的环境部署和升级其他合约。如果你对高级用例感兴趣,我们正在编写与部署相关的指南。
部署合约后,我们建议使用 Defender 监控其状态和交易。了解如何使用 Monitor here。 |