- A+
Defender 允许你轻松地跨链部署和升级智能合约,同时保持最佳的安全实践。本教程展示了如何使用一个 Relayer 来部署一个名为 Box 的合约,并通过一个 Safe wallet(多签钱包)使用 UUPS 代理模式进行升级。
前提条件
- OpenZeppelin Defender 账户。你可以在这里 注册 Defender。
- NodeJS 和 NPM
- 任何 IDE 或文本编辑器
- 带有 Metamask(或任何其他兼容钱包)的网络浏览器,并且有 Sepolia ETH 资金。
1. 配置
Safe wallet
首先,你需要创建 Safe wallet 来管理升级过程。请按照以下步骤操作:
- 在网络浏览器中打开 Safe app 并连接你的钱包(确保你已连接到 Sepolia 测试网 )。
- 点击 Create new Account 并按照步骤操作。
- 记下你创建的 Safe wallet 地址,稍后你将需要它。
环境设置
现在,你将创建一个带有 Sepolia 测试网的 Defender 测试环境,在这里你将部署和升级智能合约。请按照以下步骤操作:
- 打开 Defender Deploy。
- 点击 Setup。
- 从下拉菜单中选择 Sepolia。
- 选择与你的资金 relayer 相关联的审批流程,该 relayer 将为测试环境执行部署。如果你还没有审批流程,Defender 将允许你在向导流程中创建一个。Relayers 自动支付 gas 费用,并负责私钥安全存储、交易签名、nonce 管理、gas 价格估算和重新提交。不过,你也可以选择使用 EOA(“外部拥有账户”)或 Safe wallet 进行部署。
阅读更多关于 relayers 及其管理的信息这里 。
- 点击审批流程字段以展开下拉菜单,然后点击 Create an Approval Process。输入“Safe Wallet Approval Process”作为名称,展开合约字段并点击 Add contract。输入“Safe Wallet”作为名称,粘贴你之前复制的 Safe wallet 地址并点击 Create。在合约下拉菜单中选择“Safe Wallet”并点击 Continue。
- Defender 将为此环境生成一个 API Key 和 Secret,请复制并安全存储它们。点击 Let’s Deploy 访问环境页面。
你已配置测试环境,以便在没有实际资金损失风险的情况下学习。设置生产环境的步骤是相同的。
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 设置
首先,确保你已安装 Hardhat 和 ethers v6。按照以下步骤创建一个新目录和项目:
- 在终端中运行以下命令:
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
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;
- 在项目根目录中创建一个名为
.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 {}
}
这是一个模拟盒子的合约,具有三个功能:
* `initialize()`: 使用其初始实现初始化可升级代理,并将 multisig 设置为所有者。
* `removeObject()`: 通过移除一个对象来减少盒子中的对象数量。
* `_authorizeUpgrade()`: 将代理指向新的实现地址。
Foundry
- 在
script
目录下创建一个名为Deploy.s.sol
的文件。此脚本将通过 Defender 部署可升级的 Box 合约,初始包含 5 个对象,所有者为环境设置中配置的 multisig 地址。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 个对象,所有者为环境设置中配置的 multisig 地址。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`。更多信息请参见[这里](https://github.com/OpenZeppelin/openzeppelin-upgrades/blob/master/docs/modules/ROOT/pages/defender-deploy.adoc) 。
2. 通过运行以下命令来部署你的 box,它将执行你的部署脚本:
npx hardhat run --network sepolia scripts/deploy.ts
成功!你的合约应该已经部署在 Sepolia 测试网上。导航到 Defender 的 Deploy 页面,检查代理和实现是否已在测试环境中部署。所有 Box 交易都应发送到代理地址,因为它将存储状态并指向给定的实现。复制代理地址以便下一步升级。
注意事项
默认情况下,Defender 使用 CREATE
操作码来部署合约。此方法创建一个新的合约实例并为其分配一个唯一地址。此地址由交易的 nonce 和发送者的地址决定。
Defender 还提供了使用 CREATE2
操作码的高级部署选项。当部署请求包含 salt
时,Defender 切换为使用 CREATE2
操作码。此操作码允许你根据发送者的 address
、salt
和合约 bytecode
的组合来部署到一个确定的地址。
虽然 CREATE2
提供了确定性的合约地址,但它改变了 msg.sender
的行为。在 CREATE2
部署中,构造函数或初始化代码中的 msg.sender
指的是工厂地址,而不是标准 CREATE
部署中的部署地址。这一区别可能会影响合约逻辑,因此在选择 CREATE2
时建议进行仔细测试和考虑。
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;
}
}
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}`);
}
// 我们推荐这种模式,以便能够在任何地方使用 async/await
// 并正确处理错误。
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
- 使用升级脚本创建升级提案,使用以下命令:
npx hardhat run --network sepolia scripts/upgrade.ts
Approve
- 导航到 Defender 测试环境 并点击升级提案,这将在屏幕右侧展开一个模态窗口。
- 点击 View Transaction Proposal 并点击页面右上角的 Approve and Execute。使用你创建 Safe Wallet 时使用的钱包签名并执行交易。
你的 box 现在应该已经升级到新版本!测试环境页面中的升级提案现在应该标记为 Executed。
下一步
恭喜!你现在可以使用相同的环境部署和升级其他合约。如果你对高级用例感兴趣,我们正在编写与部署相关的指南。
在部署合约后,我们建议使用 Defender 监控其状态和交易。 在 这里了解如何使用 Monitor。
参考资料
- Deploy Documentation
- Foundry Upgrades Package
- Hardhat Upgrades Package
- Upgrades Core Package
- 我的微信
- 这是我的微信扫一扫
-
- 我的电报
- 这是我的电报扫一扫
-