用 OpenZeppelin 和 Foundry 创建和部署可升级的 ERC20 代币
- 原文链接:Deploy an Upgradeable ERC20 Token
- 译文出自:登链翻译计划
- 译者:翻译小组 ,校对:Tiny 熊
- 本文永久链接:learnblockchain.cn/article…
在部署后能够调整和改进智能合约的能力变得至关重要。随着项目的发展,可能会出现意想不到的需求、潜在的优化或不同的产品。对许多人来说,传统的部署全新合约并迁移用户的方法不仅不切实际,而且可能导致用户的沮丧和信任流失。这就是可升级合约的威力所在,它允许你在保留其地址和用户数据的同时改进代币的逻辑。在本教程中,我们将向你展示如何利用 OpenZeppelin 的经过审计的合约套件和 Foundry 的部署功能来创建一个可升级的 ERC-20 代币。
ERC-20 代币标准是在以太坊和基于 EVM 的区块链上创建可互换代币的蓝图。ERC-20 引入了一套标准化的规则,包括了强制性的transfer
、balanceOf
和totalSupply
等函数,确保了代币之间的一致行为。类似于面向对象编程中的接口,开发人员可以欣赏它在强制采用这些关键函数方面的作用。这种统一性已经彻底改变了代币的交互方式,使它们变得无缝和高效,现在代币可以轻松交易、在 dApps 中使用或存储在钱包中,而所有这些都是在以太坊的区块链上运行的。
在其核心,可升级的代币拥抱在部署后增强或修改其功能的灵活性。这是通过分层架构实现的:代理充当与用户交互的不可变智能合约,而逻辑智能合约(有时也称为实现合约)包含业务逻辑。升级是通过更改代理对较新逻辑合约的引用来实现的,确保代币余额和其他状态变量保持不变。在这种设计中,管理员角色通常管理升级过程,决定逻辑合约何时以及如何更改,从而在代币的生命周期中引入了一种治理元素。
有不同类型的可升级智能合约,让我们来介绍最常见的并进行比较。
透明代理模式旨在区分管理员和普通用户。它通过使用两个不同的地址来工作:一个用于管理员(可以升级合约),另一个用于普通用户(可以与合约的函数交互)。代理合约包括了区分管理员调用和普通用户调用的逻辑,防止在常规使用过程中意外执行管理功能。
UUPS(通用可升级代理标准)代理是一种更简化和更节省 gas 的方法。在这种模式中,升级功能嵌入在逻辑合约本身中。这种设计减少了对额外'管理员'合约的需求,简化了结构。但是,它也要求逻辑合约在设计时考虑到可升级性,在其中嵌入必要的升级功能。
Beacon 代理模式引入了一个中央的“信标(Beacon)”合约,所有代理实例都引用该合约以获取当前逻辑合约的地址。这种设计允许更高效的升级过程,因为在信标中更新逻辑合约地址会自动更新所有关联的代理。在需要保持多个代理合约与同一逻辑合约同步的情况下,这是特别有用的。
要了解更多关于代理的信息,请查看这个 QuickNode 指南和OpenZeppelin 代理。
在本指南的技术演示中,我们将介绍 UUPS 代理方法。
OpenZeppelin提供了一系列可重用的智能合约,这些合约是安全的并且经过了审计,确保了你的智能合约的基本构建块是安全的。对于可升级性,OpenZeppelin 提供了代理合约,将调用委托给实现合约。这种方法允许开发人员替换实现合约,同时保留代理的存储、地址和余额。
要了解更多关于 OpenZeppelin 及其可升级性插件的信息,请查看这个资源 。
要将智能合约部署到区块链,你需要一个 API 端点来与网络通信。你可以使用公共节点或部署和管理自己的基础设施;但是,如果你希望获得 8 倍更快的响应时间,你可以把繁重的工作交给我们(QuickNode)。在这里注册一个免费账户。
登录后,点击创建端点按钮,然后选择你要部署的区块链和网络。在本指南中,我们将选择以太坊 Sepolia链。
创建端点后,复制 HTTP 提供程序链接并保持方便,因为你将在接下来的部分中需要它。
如果你需要在 Sepolia 测试网上获得 ETH,Multi-Chain QuickNode Faucet 可以帮助你轻松获取测试 ETH!
转到 Multi-Chain QuickNode Faucet 并连接你的钱包(例如 MetaMask、Coinbase 钱包)或粘贴你的钱包地址以获取测试 ETH。请注意,以太坊主网需要 0.001 ETH 的余额要求才能使用 EVM 水龙头。你还可以在推特上发布你的请求以获得奖励!
在下一节中,我们将转向创建项目目录并配置项目文件和依赖项。
现在我们对 ERC-20 代币以及以太坊上的可升级性有了很好的基本理解,让我们开始编写一个实际的示例。首先,让我们安装并初始化一个 Foundry 项目。
如果你尚未安装 Foundry,请打开你的终端并运行以下命令:
curl -L https://foundry.paradigm.xyz | bash
上面的命令将安装Foundryup
。然后,根据屏幕上的指示继续操作,这将使你能够在 CLI 中使用foundryup
命令。安装完成后,你可以在终端中运行foundryup -v
命令来检查版本并安装最新的(夜间)预编译二进制文件。
提示
如果你使用的是 Windows,你需要安装并使用 Git BASH 或 WSL 作为你的终端,因为 Foundryup 目前不支持 Powershell 或 Cmd。请按照此处的说明进行操作。
或者,如果你使用的是 M1 Mac,并且出现错误:
dyld[32719]: Library not loaded: /usr/local/opt/libusb/lib/libusb-1.0.0.dylib
;请尝试通过 brew 安装该库:brew install libusb
。
配置完成后,使用以下命令初始化一个 Foundry 项目并进入该目录:
forge init erc20_upgradeable && cd erc20_upgradeable
然后,进入erc20_upgradeable目录,你的项目结构应如下所示:
.
├── lib
├── script
├── src
└── test
foundry.toml
你可能会在项目中看到一些现有的示例文件,但你可以忽略它们。
让我们回顾一下这个结构。
我们还需要创建项目所需的文件。运行以下命令以创建智能合约文件、测试文件、部署文件和 remappings.txt(我们将使用它来正确映射我们的库依赖项)。
echo > src/MyToken.sol && echo > src/MyTokenV2.sol && echo > test/MyTokenTest.t.sol && echo > script/deployToken.s.sol && echo > script/deployProxy.s.sol && echo > remappings.txt
在下一节中,我们将安装所需的库并设置我们的配置。
通过初始化我们的项目,让我们安装本指南中将要使用的 OpenZeppelin 库。在项目的根目录中,在你的终端中运行以下命令:
forge install OpenZeppelin/openzeppelin-contracts --no-commit
forge install OpenZeppelin/openzeppelin-foundry-upgrades --no-commit
forge install OpenZeppelin/openzeppelin-contracts-upgradeable --no-commit
注意末尾的--no-commit
标志。这是因为你的项目文件夹已经与 git 存储库关联,所以我们必须指定不提交任何内容。
现在,让我们通过填写我们之前创建的remappings.txt文件来将导入配置到正确的路径。
向文件中添加以下配置:
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
保存文件后,让我们打开foundry.toml文件,并向你的文件添加以下代码:
build_info = true
extra_output = ["storageLayout"]
[rpc_endpoints]
sepolia = "QUICKNODE_ENDPOINT_URL"
前两行(例如,build_info
和extra_output
)是在使用 OpenZeppelin Foundry Upgrades library(感谢 ericglau!)时所需的配置。此外,由于我们在本指南中在 Sepolia 测试网上部署,我们将把这个端点命名为sepolia
。如果你在其他网络上部署,可以更改名称。注意,请记住将QUICKNODE_ENDPOINT_URL占位符替换为你之前创建的实际 QuickNode HTTP 提供程序 URL。
最后,让我们在环境中设置我们的私钥,使用以下变量名和你的私钥。在你的终端中运行以下命令,并将YOUR_PRVATE_KEY占位符更新为你的实际私钥。
export PRIVATE_KEY=YOUR_PRIVATE_KEY
配置设置完成后,让我们继续创建可升级的 ERC-20 代币。
是时候为可升级的 ERC-20 代币合约构建逻辑了。在我们开始编写代码之前,让我们先了解一下我们的 ERC-20 代币将具有的确切功能。
我们将在 ERC-20 代币中继承不同的智能合约:
ERC20Upgradeable
- 包含可升级功能的 ERC-20 代币OwnableUpgradeable
- 仅允许所有者执行某些功能(所有者可以被转移)ERC20PermitUpgradeable
- 添加了一个许可功能,用户可以使用它来节省离线批准的成本Initializable
- 类似于构造函数,我们将使用它来设置代币的初始参数UUPSUpgradeable
- 我们的 ERC-20 代币将继承的通用可升级代理标准模式逻辑现在,进入你的src文件夹,并打开MyToken.sol文件。更新文件内容以包括:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol";
import "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol";
import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
contract MyToken is Initializable, ERC20Upgradeable, OwnableUpgradeable, ERC20PermitUpgradeable, UUPSUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address initialOwner) initializer public {
__ERC20_init("MyToken", "MTK");
__Ownable_init(initialOwner);
__ERC20Permit_init("MyToken");
__UUPSUpgradeable_init();
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function _authorizeUpgrade(address newImplementation)
internal
onlyOwner
override
{}
}
为了下次轻松部署,请查看Remix.IDE
记得保存文件。让我们回顾一下代码。
如上所述,在我们的 ERC-20 代币中继承了不同的智能合约。然后,在合约的constructor
中,我们通过调用_disableInitializers()
确保初始化方法只运行一次,以防止意外重新初始化。中间的initialize
函数设置了代币的名称为"MyToken"和符号为"MTK",将所有权分配给提供的initialOwner
,并激活了增强的授权机制和可升级功能。还向调用此函数的用户铸造了初始代币供应。此外,合约提供了一个仅限所有者的mint
函数,允许创建新代币。通过内部的_authorizeUpgrade
方法,确保了安全的合约升级,只允许所有者授权新的合约版本。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol";
import "openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol";
import "openzeppelin-contracts-upgradeable/contracts/token/ERC20/extensions/ERC20PermitUpgradeable.sol";
import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
/// @custom:oz-upgrades-from MyToken
contract MyTokenV2 is Initializable, ERC20Upgradeable, OwnableUpgradeable, ERC20PermitUpgradeable, UUPSUpgradeable {
/// @custom:oz-upgrades-unsafe-allow constructor
constructor() {
_disableInitializers();
}
function initialize(address initialOwner) initializer public {
__ERC20_init("MyTokenV2", "MTKV2");
__Ownable_init(initialOwner);
__ERC20Permit_init("MyTokenV2");
__UUPSUpgradeable_init();
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function _authorizeUpgrade(address newImplementation)
internal
onlyOwner
override
{}
}
总的来说,上面的 ERC-20 智能合约与MyToken.sol非常相似,但存在差异,例如不同的合约名称、符号,并包含升级我们的合约所需的 Foundry/OpenZepplin 注释(例如,/// @custom:oz-upgrades-from MyToken
)。
现在,让我们进行编译和测试。
有了我们编写的智能合约,让我们尝试编译合约并测试可升级的 ERC-20 代币逻辑的行为。
我们将测试以下功能:
铸造
功能(请注意,这仅限于Owner
)现在,转到test文件夹并打开MyTokenTest.t.sol文件。正如你可能已经猜到的那样,Foundry 中的测试是用 Solidity 编写的。更新文件的内容以包含以下内容:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/MyToken.sol";
import "forge-std/console.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
import { Upgrades } from "openzeppelin-foundry-upgrades/Upgrades.sol";
contract MyTokenTest is Test {
MyToken myToken;
ERC1967Proxy proxy;
address owner;
address newOwner;
// Set up the test environment before running tests
function setUp() public {
// 部署实现
MyToken implementation = new MyToken();
// Define the owner address
owner = vm.addr(1);
// Deploy the proxy and initialize the contract through the proxy
proxy = new ERC1967Proxy(address(implementation), abi.encodeCall(implementation.initialize, owner));
// 用代理关联 MyToken 接口
myToken = MyToken(address(proxy));
// Define a new owner address for upgrade tests
newOwner = address(1);
// Emit the owner address for debugging purposes
emit log_address(owner);
}
// Test the basic ERC20 functionality of the MyToken contract
function testERC20Functionality() public {
// Impersonate the owner to call mint function
vm.prank(owner);
// Mint tokens to address(2) and assert the balance
myToken.mint(address(2), 1000);
assertEq(myToken.balanceOf(address(2)), 1000);
}
// 测试升级
function testUpgradeability() public {
// Upgrade the proxy to a new version; MyTokenV2
Upgrades.upgradeProxy(address(proxy), "MyTokenV2.sol:MyTokenV2", "", owner);
}
}
测试代码相当长,但我们已添加了注释,以便你更好地理解每个测试用例的发生情况。
现在,要编译我们的合约并执行测试,请在终端中运行以下命令:
forge build && forge test --ffi
包括
--ffi
标志是为了运行我们的代码需要访问的外部脚本。
你可能会收到一些警告,例如“源文件未指定所需的编译器版本!”但这可以忽略。
你将看到类似以下的输出:
[⠢] Compiling...
[⠃] Compiling 62 files with 0.8.22
[⠰] Solc 0.8.22 finished in 3.79s
Compiler run successful with warnings:
Warning (3420): Source file does not specify required compiler version! Consider adding "pragma solidity ^0.8.22;"
--> script/deployProxy.s.sol
Warning (3420): Source file does not specify required compiler version! Consider adding "pragma solidity ^0.8.22;"
--> script/deployToken.s.sol
[⠢] Compiling...
No files changed, compilation skipped
Running 2 tests for test/MyTokenTest.t.sol:MyTokenTest
[PASS] testERC20Functionality() (gas: 48673)
[PASS] testUpgradeability() (gas: 1642043)
Test result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.43s
Running 2 tests for test/Counter.t.sol:CounterTest
[PASS] testFuzz_SetNumber(uint256) (runs: 256, μ: 27709, ~: 28409)
[PASS] test_Increment() (gas: 28379)
Test result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.43s
Ran 2 test suites: 4 tests passed, 0 failed, 0 skipped (4 total tests)
注意:如果你在测试中遇到错误,你可能需要运行命令 -
forge clean && forge build && forge test --ffi
以删除构建工件和缓存目录,然后重新编译并运行测试。
此外,你将注意到两个新目录;out
目录包含合约工件,例如 ABI,而cache
文件夹被 forge 用于重新编译必要的内容。
唯一剩下的就是部署可升级的 ERC-20 代币。让我们开始吧!
Foundry 通过使用forge create
命令使通过 CLI 轻松部署智能合约;但是,你也可以使用脚本进行部署。请注意,Foundry 一次只能部署一个合约,但这对我们目前来说并不构成障碍。
要部署 ERC-20 代币,让我们使用一个脚本。打开我们之前创建的scripts/deployToken.s.sol文件,并更新文件以使用以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../src/MyToken.sol";
import "forge-std/Script.sol";
contract DeployTokenImplementation is Script {
function run() public {
// Use address provided in config to broadcast transactions
vm.startBroadcast();
// Deploy the ERC-20 token
MyToken implementation = new MyToken();
// Stop broadcasting calls from our address
vm.stopBroadcast();
// Log the token address
console.log("Token Implementation Address:", address(implementation));
}
}
记得保存文件!剩下的就是执行脚本。
forge script script/DeployToken.s.sol --rpc-url sepolia --private-key $PRIVATE_KEY --broadcast
上述命令使用我们在foundry.toml
文件中配置的sepolia
RPC URL 执行脚本。--private-key
标志设置我们用于交易的账户,--broadcast
命令用于将它们广播到网络中。
提示
如果你想要验证你的合约在 Etherscan 上,以便让你和其他人能够从区块浏览器读取和写入你的智能合约,你将需要在上述命令的末尾添加
--etherscan-api-key YOUR_ETHERSCAN_API_KEY --verify
标志。在本指南的结尾,我们将演示如何从代理中读取,因此如果你想执行该步骤,这将是必需的。
成功后,你将看到如下输出:
== Logs ==
Token Implementation Address: 0x195136BA4F105dAe042F96a59E4dbeF9DCAdE773
...
...
✅ [Success]Hash: 0xe097b9397cd7d36bcf8dc379c95b511746b8d6802e4794e4b0b1125e36bf75bf
Contract Address: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Block: 1
Paid: 0.00673602 ETH (1684005 gas * 4 gwei)
接下来,让我们部署代理合约。
打开scripts/deployProxy.s.sol文件并输入以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "../src/MyToken.sol";
import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import "forge-std/Script.sol";
contract DeployUUPSProxy is Script {
function run() public {
address _implementation = YOUR_DEPLOYED_SMART_CONTRACT_ADDRESS; // Replace with your token address
vm.startBroadcast();
// Encode the initializer function call
bytes memory data = abi.encodeWithSelector(
MyToken(_implementation).initialize.selector,
msg.sender // Initial owner/admin of the contract
);
// Deploy the proxy contract with the implementation address and initializer
ERC1967Proxy proxy = new ERC1967Proxy(_implementation, data);
vm.stopBroadcast();
// Log the proxy address
console.log("UUPS Proxy Address:", address(proxy));
}
}
重要提示:现在,在运行下一个命令之前,你需要使用你从上一步骤部署的智能合约地址(例如,代币地址)更新_implementation
变量。记得保存文件。
然后,要部署代理,请运行以下命令:
forge script script/deployProxy.s.sol:DeployUUPSProxy --rpc-url sepolia --private-key $PRIVATE_KEY --broadcast
请记住,可选地,你还可以在上述命令的末尾添加--etherscan-api-key YOUR_ETHERSCAN_API_KEY --verify
标志以验证你的合约。如果你想要在接下来的步骤中与你的智能合约交互,这将是必需的。
花点时间通过查看 Etherscan 来验证你的智能合约是否已部署。你可以通过 Etherscan 验证合约是否是代理,方法是导航到Code选项卡,单击More Options下拉菜单,然后选择Is this a proxy?。你将被提示验证地址(单击继续),然后导航回 Code 选项卡,你应该会看到两个新选项卡,Read as Proxy和Write as Proxy。如果需要,你可以通过 Etherscan 在这些选项卡上与你的 ERC-20 代币合约进行交互。
例如,让我们使用Read as Proxy选项卡查看地址的 ERC-20 代币余额:
在上面的图像中,我们正在检查所有者的余额,因为我们在部署时向其铸造了代币。
你现在可以采取的下一步是与你的智能合约进行交互。由于我们在测试部分已经涵盖了交互,所以我们将暂时跳过这部分。
在结束本教程之前,让我们看看如何可以使用 QuickNode 的 Token API 轻松获取 ERC-20 代币元数据和交易。
要使用 Token API,你可以使用 QuickNode SDK 或在你喜欢的 web3 SDK(如 ethers.js 和 Eth.go)中实现它(在此处查看文档 )。
以下是一个快速示例,演示如何使用 cURL 获取你的 ERC-20 代币的元数据:
curl QUICKNODE_ENDPOINT_URL \
-X POST \
-H "Content-Type: application/json" \
--data '{
"id":67,
"jsonrpc":"2.0",
"method":"qn_getTokenMetadataByContractAddress",
"params": [{
"contract": "YOUR_TOKEN_ADDRESS"
}]
}'
只需将代码粘贴到你的终端,并记得用你的实际 HTTP 提供程序 URL 和代币地址替换QUICKNODE_ENDPOINT_URL
和YOUR_TOKEN_ADDRESS
。
通过在我们的 ERC-20 代币上调用qn_getTokenMetadataByContractAddress RPC 方法,我们可以返回诸如其元数据和交易信息(例如创建代币的创世块)之类的代币详细信息。
示例响应:
{
"jsonrpc": "2.0",
"id": 67,
"result": {
"name": "MyToken",
"symbol": "MTK",
"contractAddress": "0xc731bc16e15e97687130f4c9a7232781ea060040",
"decimals": "18",
"genesisBlock": "4701990",
"genesisTransaction": "0xa8c93e0c5108f73a039e1537b02f94e871398b6b3fe3f4efafc97c8782965b8a"
}
}
QuickNode 还提供 NFT API,允许你检索聚合的 NFT 数据,例如集合详细信息、转移历史、元数据等。这两个 API 都受 QuickNode 的 Graph API 支持,该 API 允许你查询相同的数据,但以灵活的响应形式,并支持其他聚合数据,例如历史交易数据(例如 OHLC)和频繁更改的数据的实时订阅。
给自己一个鼓励吧!你已经完成了这篇关于创建和部署可升级 ERC-20 代币的技术指南。在这个过程中,你回顾了可升级智能合约的概念以及它们的不同类型,然后设置了一个智能合约环境来创建、测试和部署你的可升级 ERC-20 代币。
本翻译由 DeCert.me 协助支持, 在 DeCert 构建可信履历,为自己码一个未来。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!