本文档介绍了如何使用 Chainlink CCIP 在 Hyperliquid 上搭建代币桥。通过 Foundry 部署 ERC-20 代币、设置 CCIP 代币池,并在 HyperEVM 和 BSC 之间执行 qWHYPE 代币的跨链转移。主要步骤包括部署代币合约、部署 CCIP 代币池合约、注册和配置管理员角色,以及进行跨链转账。
Hyperliquid 将两个层集成在一个统一的状态下:HyperCore,一个通过签名操作访问的超低延迟订单簿引擎;以及 HyperEVM,一个完全兼容 EVM 的网络,用于标准的 Solidity 开发。
Chainlink CCIP 允许使用 burn-and-mint 机制在链之间桥接资产。在 Hyperliquid 中,HyperCore(HIP-1)资产及其 HyperEVM 表示之间的转换由原生协议流程处理。
在本指南中,你将使用 Foundry 封装一个对应于 HIP-1 资产的 HyperEVM token,并使用 CCIP 在 HyperEVM 和 BNB Smart Chain (BSC) 之间桥接该 token。
免责声明
在本指南中,我们使用每个链的 MAINNET。这是因为在撰写本文时,Chainlink CCIP 在 HyperEVM Testnet 上不可用。请谨慎操作,只使用你可以承受损失的资金。
我们将使用 Foundry 来编译、部署和交互我们的智能合约。如果你还没有安装 Foundry,你可以通过在终端中运行以下命令来安装:
curl -L https://foundry.paradigm.xyz | bash
按照屏幕上的说明操作,之后你就可以使用 foundryup
命令来安装 Foundry。确保你在一个新的终端会话中执行此命令,该会话考虑了对你的 PATH
变量的更改。
foundryup
我们需要在本地创建一个新文件夹,用于存放本指南的项目。我们将我们的文件夹命名为 hyperevm_ccip,但你可以随意命名。在终端中运行以下命令来创建文件夹,并使用你的代码编辑器导航到该文件夹中。在本指南中,我们将使用 VS Code。
forge init hyperevm_ccip
cd hyperevm_ccip
code .
此时,你的设置应该如下所示:
太棒了!我们的 Foundry 项目结构现在已经设置好了。接下来,在你的项目文件夹的根目录下创建一个 .env
文件。我们将在其中存储我们的环境变量,例如我们的私钥和 RPC URL。以下是 .env
文件的格式:
HYPEREVM_RPC="your_hyperevm_rpc_url"
BSC_RPC="your_bsc_rpc_url"
PRIVATE_KEY="your_private_key"
ETHERSCAN_API_KEY="your_etherscan_api_key"
我们将在稍后讨论这些变量的含义以及如何获取它们。下一步是删除 Foundry 为我们生成的默认的 src/Counter.sol
合约和 script/Counter.s.sol
脚本。我们不会在本指南中使用它们。或者,你可以在终端中运行以下命令来删除这些文件:
rm src/Counter.sol
rm script/Counter.s.sol
你还需要配置项目文件夹根目录下的 foundry.toml
文件,以包含 Solidity 编译器的重要设置以及我们将要使用的导入语句的重映射。以下是你的 foundry.toml
文件应该是什么样子:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
optimizer = true
optimizer_runs = 200
remappings = [\
'@chainlink/contracts-ccip/=node_modules/@chainlink/contracts-ccip/',\
'@chainlink/contracts/=node_modules/@chainlink/contracts/',\
]
fs_permissions = [{ access = "read-write", path = "./" }]
在这个文件中,我们添加了 optimizer
设置来优化我们的智能合约以进行部署。fs_permissions
条目允许 Foundry 读取和写入项目目录中的文件。remappings
条目是 Chainlink 合约包的路径,我们将在本指南中使用它们。我们将下一步安装这些包。
我们将在本指南中使用两个 Chainlink 合约包:@chainlink/contracts-ccip
和 @chainlink/contracts
。第一个包包含我们将要交互的核心 CCIP 合约以及我们将要部署的 CCIP token 池合约。第二个包包含符合 Chainlink 跨链传输要求的定制 ERC20
合约。
我们还将安装 @layerzerolabs/hyperliquid-composer
包,它将用于增加我们在 HyperEVM 上的交易的 gas limit。HyperEVM 具有一个多区块架构,该架构由 gas limit 为 200 万 gas 单位的较小区块和 gas limit 为 3000 万 gas 单位的较大区块组成。较小区块的区块时间为 1 秒,而较大区块的区块时间为 1 分钟。默认情况下,HyperEVM 上的帐户只能发送符合较小区块 gas limit 的交易。此包将允许我们启用我们的帐户以发送具有较高 gas limit 的交易,以适应较大的区块。
要安装这些包,请使用你喜欢的包管理器输入以下命令:
npm install @chainlink/contracts-ccip @chainlink/contracts @layerzerolabs/hyperliquid-composer
yarn add @chainlink/contracts-ccip @chainlink/contracts @layerzerolabs/hyperliquid-composer
太棒了!你已经成功安装了我们将在本指南中使用的依赖项。接下来,我们将讨论我们需要在 .env
文件中填充的环境变量。
首先,我们需要获取 HyperEVM 和 BSC 的适当 RPC 端点。你可以从 QuickNode 获取这些端点。只需注册一个免费试用版,创建一个新的多链端点,然后复制每个链的 HTTPS URL。将相应的 URL 粘贴到你的 .env
文件中的 HYPEREVM_RPC
和 BSC_RPC
变量中。
接下来,转到你在 Etherscan 上的个人资料,然后导航到 API KEYS 标签。如果你没有帐户,请在此处创建一个。在这里,你将创建一个 API 密钥,该密钥将帮助你在链上验证你的智能合约。复制 API 密钥并将其粘贴到你的 .env
文件中的 ETHERSCAN_API_KEY
变量中。
Etherscan API v2
Etherscan 已将其 API 升级到 v2。当你在 Etherscan 上创建 API 密钥时,它还可以用于 Etherscan 支持的多个链,包括 HyperEVM 和 BSC。因此,你不需要为每个链创建单独的 API 密钥。
最后,进入你的 MetaMask 并复制你的其中一个帐户的私钥。要了解如何访问你的私钥,请查看这个简短的指南。将此私钥粘贴到你的 .env
文件中的 PRIVATE_KEY
变量中。
这样,你的 .env
文件中的所有环境变量现在应该都已填充。此设置过程的最后一步是将适当的网络添加到我们的 MetaMask 钱包。
我们将把 HyperEVM Mainnet 和 Binance Smart Chain Mainnet 网络添加到我们的 MetaMask 钱包。添加这些网络的最简单方法是转到 hyperevmscan.io 和 bscscan.com,然后单击页面左下角的 Add
按钮。这将把网络添加到你的 MetaMask 钱包:
恭喜!你已成功为此指南设置了你的开发环境。我们的下一步是将 HIP-1 资产从 HyperCore 桥接到 HyperEVM。
你现在将把 HIP-1 资产从 HyperCore 桥接到 HyperEVM。此过程涉及将 HIP-1 资产转换为其相应的 HyperEVM token 表示。在本指南中,我们将使用 Hyperliquid 的原生生态系统币:HYPE。前往 Hyperliquid 交易界面 并使用 MetaMask 登录以开始使用。
如果你还没有 HYPE 币,你可以通过将来自 Arbitrum Mainnet 的 USDC 存入 Hyperliquid,然后在交易界面上的 Spot 上将其交易为 HYPE 来获得一些:
提醒
在本指南中,我们使用每个链的 Mainnet。这是因为在编写本文时,Chainlink CCIP 在 HyperEVM testnet 上不可用。请谨慎操作,只使用你可以承受损失的资金。
一旦你有一些 HYPE,你可以继续将其桥接到 HyperEVM。为此,请单击图表下方页面底部的 Transfer to/from EVM 按钮。只需输入你要桥接的 HYPE 数量,然后单击 Confirm。
将 HYPE 桥接到 HyperEVM
太棒了!你已成功将 HYPE 从 HyperCore 桥接到 HyperEVM。你现在应该能够在 HyperEVM 网络上的 MetaMask 中看到你的 HYPE 余额。我们现在将继续为 HYPE 创建一个符合 Chainlink 跨链传输要求的包装合约。
为了使用 Chainlink CCIP 在 HyperEVM 和 BSC 之间桥接 HYPE,我们需要创建一个符合 Chainlink 跨链传输要求的包装合约。这涉及到创建一个新的 ERC20 token 合约,该合约包装 HyperEVM 上现有的 HYPE 资产,以及 BSC 上相应的 ERC20 token 合约。
我们将创建一个符号为 qWHYPE 的 QuickNode Wrapped Hype token。输入以下命令以在你的项目目录的 src
文件夹中创建一个新的 Solidity 文件,并将以下代码粘贴到其中。
touch src/qWHYPE.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {BurnMintERC20} from "@chainlink/contracts/src/v0.8/shared/token/ERC20/BurnMintERC20.sol";
contract qWHYPE is BurnMintERC20 {
event Deposit(address indexed account, uint256 amount);
event Withdraw(address indexed account, uint256 amount);
constructor() BurnMintERC20("QuickNode Wrapped HYPE", "qWHYPE", 18, 0, 0) {}
function deposit() public payable {
_mint(msg.sender, msg.value);
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 amount) external {
_burn(msg.sender, amount);
payable(msg.sender).transfer(amount);
emit Withdraw(msg.sender, amount.value);
}
}
让我们分解 qWHYPE.sol
合约中的代码。合约从 @chainlink/contracts
包中导入 BurnMintERC20
合约,该合约提供了铸造和销毁 token 所需的功能。当用户将 HYPE 存入合约时,它会向他们的地址铸造等量的 qWHYPE token。相反,当用户提取 qWHYPE token 时,合约会销毁 token 并将相应数量的 HYPE 转回用户的地址。
这个特定的合约将部署在 HyperEVM 网络上。在 BSC 上,我们将直接部署基本的 BurnMintERC20
合约,因为它不需要任何额外的功能。我们将在下一节中使用脚本部署两个合约。
我们将编写几个 Foundry 脚本来部署和交互我们的智能合约。本节将分为几个部分来详细介绍每个脚本。我们将创建以下脚本并按此顺序执行它们:
脚本名称 | 描述 |
---|---|
DeployTokens.s.sol |
在 HyperEVM 上部署 qWHYPE 合约,并在 BSC 上部署基本的 BurnMintERC20 合约 |
DeployPools.s.sol |
在 HyperEVM 和 BSC 上部署 CCIP token 池合约 |
SetupAdmin.s.sol |
在每个链的 CCIP 合约上注册和配置管理角色 |
ConfigurePools.s.sol |
配置我们的 CCIP token 池,以便在彼此之间进行跨链传输 |
DepositAndTransferTokens.s.sol |
在 qWHYPE 合约上存入少量 HYPE 以铸造 qWHYPE token,然后执行到 BSC 的跨链传输 |
TransferTokens.s.sol |
从任一方向执行 qWHYPE token 的跨链传输 |
在我们编写脚本之前,我们需要一种方法来跟踪我们部署的合约地址,以及一个文件来存储我们将在整个脚本中使用的通用常量。
执行以下命令来创建适当的文件:
touch script/Constants.s.sol
mkdir -p script/output
touch script/output/deployments.json
我们将使用 deployments.json
文件来跟踪每个链上部署的合约地址。
以下是 Constants.s.sol
文件的样子:
单击以展开代码
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Script} from "forge-std/Script.sol";
contract Constants is Script {
struct CCIPConstants {
uint64 chainSelector;
address router;
address rmnProxy;
address tokenAdminRegistry;
address registryModuleOwnerCustom;
string nativeCurrencySymbol;
}
function getCCIPConstants(uint256 chainId) public pure returns (CCIPConstants memory) {
if(chainId == 999) {
return CCIPConstants({
chainSelector: 2442541497099098535,
router: 0x13b3332b66389B1467CA6eBd6fa79775CCeF65ec,
rmnProxy: 0x07f15e9813FBd007d38CF534133C0838f449ecFA,
tokenAdminRegistry: 0xcE44363496ABc3a9e53B3F404a740F992D977bDF,
registryModuleOwnerCustom: 0xbAb3aBB5F29275065F2814F1f4B10Ffc1284fFEf,
nativeCurrencySymbol: "HYPE"
});
} else if (chainId == 56) {
return CCIPConstants({
chainSelector: 11344663589394136015,
router: 0x34B03Cb9086d7D758AC55af71584F81A598759FE,
rmnProxy: 0x9e09697842194f77d315E0907F1Bda77922e8f84,
tokenAdminRegistry: 0x736Fd8660c443547a85e4Eaf70A49C1b7Bb008fc,
registryModuleOwnerCustom: 0x47Db76c9c97F4bcFd54D8872FDb848Cab696092d,
nativeCurrencySymbol: "BNB"
});
}
revert("Chain not supported");
}
}
此脚本将在 HyperEVM 上部署 qWHYPE
合约,并在 BSC 上部署基本的 BurnMintERC20
合约。在你的项目目录的 script
文件夹中创建一个名为 DeployTokens.s.sol
的新文件,并将以下代码粘贴到其中。
单击以展开代码
创建文件:
touch script/DeployTokens.s.sol
将以下代码粘贴到文件中:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {qWHYPE} from "../src/qWHYPE.sol";
import {BurnMintERC20} from "@chainlink/contracts/src/v0.8/shared/token/ERC20/BurnMintERC20.sol";
contract DeployTokens is Script {
string internal constant OUTPUT_PATH = "script/output/deployments.json";
/// forge script script/DeployTokens.s.sol:DeployTokens
function run() external {
// 加载环境变量
bytes memory hyperevm = bytes(vm.envString("HYPEREVM_RPC"));
bytes memory bsc = bytes(vm.envString("BSC_RPC"));
uint256 pk = vm.envUint("PRIVATE_KEY");
// 检查是否设置了环境变量
require(hyperevm.length != 0, "HYPEREVM_RPC 未设置");
require(bsc.length != 0, "BSC_RPC 未设置");
// 打印部署者地址
address deployer = vm.addr(pk);
console.log("部署者地址:", deployer);
// 在两条链上部署
address hyperAddr = deployOn(hyperevm, deployer, pk);
address bscAddr = deployOn(bsc, deployer, pk);
// 将部署的地址写入 JSON 文件
string memory obj = vm.serializeString("deployments", "qWHYPE_hyperevm", vm.toString(hyperAddr));
obj = vm.serializeString("deployments", "qWHYPE_bsc", vm.toString(bscAddr));
vm.writeJson(obj, OUTPUT_PATH);
}
function deployOn(bytes memory rpc, address deployer, uint256 pk) internal returns (address) {
vm.selectFork(vm.createFork(string(rpc)));
vm.startBroadcast(pk);
string memory chainName = getChainName(block.chainid);
address tokenAddr = address(block.chainid == 999 ? new qWHYPE() : new BurnMintERC20("QuickNode Wrapped HYPE", "qWHYPE", 18, 0, 0));
console.log("\n在", chainName, "上将 qWHYPE 部署到:", tokenAddr);
BurnMintERC20(tokenAddr).grantMintAndBurnRoles(deployer);
console.log("在", chainName, "qWHYPE 上授予 minter 和 burner 角色给:", deployer);
vm.stopBroadcast();
return tokenAddr;
}
```function getChainName(uint256 chainId) internal pure returns (string memory) {
if (chainId == 56) return "\x1b[36mBSC Mainnet\x1b[0m";
else if (chainId == 999) return "\x1b[32mHyperEVM Mainnet\x1b[0m";
else revert("Unsupported chain ID");
}
}
在这个脚本中,我们首先加载 RPC URL 和私钥的环境变量。然后我们定义一个 deployOn
函数,它接受 RPC URL、部署者地址和私钥作为参数。此函数创建指定链的一个 fork,开始使用提供的私钥广播交易,并部署 qWHYPE
合约(在 HyperEVM 上)或基础 BurnMintERC20
合约(在 BSC 上)。部署后,它会授予部署者地址在已部署的 token 合约上的铸币和销毁角色。
此脚本将在 HyperEVM 和 BSC 上部署 CCIP token 池合约。在你的项目目录的 script
文件夹中创建一个名为 DeployPools.s.sol
的新文件,并将下面的代码粘贴到其中。
点击展开代码
创建文件:
touch script/DeployPools.s.sol
将下面的代码粘贴到文件中:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Script} from "forge-std/Script.sol";
import {BurnMintERC20} from "@chainlink/contracts/src/v0.8/shared/token/ERC20/BurnMintERC20.sol";
import {IBurnMintERC20} from "@chainlink/contracts/src/v0.8/shared/token/ERC20/IBurnMintERC20.sol";
import {BurnMintTokenPool} from "@chainlink/contracts-ccip/contracts/pools/BurnMintTokenPool.sol";
import {Constants} from "./Constants.s.sol";
contract DeployPools is Script {
string internal constant OUTPUT_PATH = "script/output/deployments.json";
/// forge script script/DeployPools.s.sol:DeployPools
function run() external {
// Load env vars
bytes memory hyperevm = bytes(vm.envString("HYPEREVM_RPC"));
bytes memory bsc = bytes(vm.envString("BSC_RPC"));
uint256 pk = vm.envUint("PRIVATE_KEY");
// Check if env vars are set
require(hyperevm.length != 0, "HYPEREVM_RPC not set");
require(bsc.length != 0, "BSC_RPC not set");
// Read deployed token addresses from JSON
string memory json = vm.readFile(OUTPUT_PATH);
address tokenHyp = vm.parseJsonAddress(json, ".qWHYPE_hyperevm");
address tokenBsc = vm.parseJsonAddress(json, ".qWHYPE_bsc");
// Fetch CCIP constants
Constants constants = new Constants();
Constants.CCIPConstants memory cfgHyp = constants.getCCIPConstants(999);
Constants.CCIPConstants memory cfgBsc = constants.getCCIPConstants(56);
// Create forks up front
uint256 hyperFork = vm.createFork(string(hyperevm));
uint256 bscFork = vm.createFork(string(bsc));
// Deploy pool on HyperEVM
vm.selectFork(hyperFork);
vm.startBroadcast(pk);
address poolHyp = address(new BurnMintTokenPool(IBurnMintERC20(tokenHyp), 18, new address[](0), cfgHyp.rmnProxy, cfgHyp.router));
BurnMintERC20(tokenHyp).grantMintAndBurnRoles(poolHyp);
vm.stopBroadcast();
// Deploy pool on BSC
vm.selectFork(bscFork);
vm.startBroadcast(pk);
address poolBsc = address(new BurnMintTokenPool(IBurnMintERC20(tokenBsc), 18, new address[](0), cfgBsc.rmnProxy, cfgBsc.router));
BurnMintERC20(tokenBsc).grantMintAndBurnRoles(poolBsc);
vm.stopBroadcast();
// Write deployed addresses to JSON file
string memory out = vm.serializeString("deployments", "qWHYPE_hyperevm", vm.toString(tokenHyp));
out = vm.serializeString("deployments", "qWHYPE_bsc", vm.toString(tokenBsc));
out = vm.serializeString("deployments", "qWHYPE_pool_hyperevm", vm.toString(poolHyp));
out = vm.serializeString("deployments", "qWHYPE_pool_bsc", vm.toString(poolBsc));
vm.writeJson(out, OUTPUT_PATH);
}
}
在这个脚本中,我们首先加载 RPC URL 和私钥的环境变量。然后我们从上一个脚本中创建的 deployments.json
文件中读取已部署的 token 地址。我们使用 Constants
合约为 HyperEVM 和 BSC 获取必要的 CCIP 常量。
这些常量包括 Chainlink 在每个链上部署的核心 CCIP 合约,包括 Router、TokenAdminRegistry、RegistryModuleOwnerCustom 和 RiskManagementNetwork (RMN) 代理地址,这些都是池部署所需的。Router 负责路由跨链消息,TokenAdminRegistry 管理 token 地址到其各自池的映射,RegistryModuleOwnerCustom 用于不同所有权模式的 token 管理注册,RMN 代理用于验证跨链消息的 RMN 签名。
使用这些常量,我们在两条链上部署 BurnMintTokenPool
合约,授予它们在其各自 token 合约上的铸币和销毁角色。最后,我们使用已部署的 token 池的地址更新 deployments.json
文件。
此脚本将为我们在每个链的 CCIP 注册合约上部署的 token 注册管理员。在你的项目目录的 script
文件夹中创建一个名为 SetupAdmin.s.sol
的新文件,并将下面的代码粘贴到其中。
点击展开代码
创建文件:
touch script/SetupAdmin.s.sol
将下面的代码粘贴到文件中:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {TokenAdminRegistry} from "@chainlink/contracts-ccip/contracts/tokenAdminRegistry/TokenAdminRegistry.sol";
import {RegistryModuleOwnerCustom} from "@chainlink/contracts-ccip/contracts/tokenAdminRegistry/RegistryModuleOwnerCustom.sol";
import {Constants} from "./Constants.s.sol";
contract SetupAdmin is Script {
string internal constant OUTPUT_PATH = "script/output/deployments.json";
/// forge script script/SetupAdmin.s.sol:SetupAdmin
function run() external {
// Load env vars
bytes memory hyperevm = bytes(vm.envString("HYPEREVM_RPC"));
bytes memory bsc = bytes(vm.envString("BSC_RPC"));
uint256 pk = vm.envUint("PRIVATE_KEY");
// Check if env vars are set
require(hyperevm.length != 0, "HYPEREVM_RPC not set");
require(bsc.length != 0, "BSC_RPC not set");
// Read deployed token addresses
string memory json = vm.readFile(OUTPUT_PATH);
address tokenHyp = vm.parseJsonAddress(json, ".qWHYPE_hyperevm");
address tokenBsc = vm.parseJsonAddress(json, ".qWHYPE_bsc");
// Fork both chains up front
uint256 hyperFork = vm.createFork(string(hyperevm));
uint256 bscFork = vm.createFork(string(bsc));
// Do HyperEVM
vm.selectFork(hyperFork);
_registerAndAccept(tokenHyp, pk);
// Do BSC
vm.selectFork(bscFork);
_registerAndAccept(tokenBsc, pk);
}
function _registerAndAccept(address token, uint256 pk) internal {
Constants constants = new Constants();
Constants.CCIPConstants memory cfg = constants.getCCIPConstants(block.chainid);
address ownerCustomModule = cfg.registryModuleOwnerCustom;
address tokenAdminRegistry = cfg.tokenAdminRegistry;
string memory chainName = _getChainName(block.chainid);
vm.startBroadcast(pk);
// Register admin on RegistryModuleOwnerCustom (msg.sender becomes pending admin)
RegistryModuleOwnerCustom(ownerCustomModule).registerAdminViaGetCCIPAdmin(token);
console.log("Proposed admin via OwnerCustom on", chainName);
// Accept the admin role from the pending admin
TokenAdminRegistry tokenAdminRegistryContract = TokenAdminRegistry(tokenAdminRegistry);
TokenAdminRegistry.TokenConfig memory config = tokenAdminRegistryContract.getTokenConfig(token);
address pendingAdmin = config.pendingAdministrator;
require(pendingAdmin == vm.addr(pk), "Pending admin mismatch");
tokenAdminRegistryContract.acceptAdminRole(token);
console.log("Successfully registered and accepted admin role for token on", chainName, ": ", token);
vm.stopBroadcast();
}
function _getChainName(uint256 chainId) internal pure returns (string memory) {
if (chainId == 56) return "BSC";
if (chainId == 999) return "HyperEVM";
return "Unknown";
}
}
在这个脚本中,我们首先加载 RPC URL 和私钥的环境变量。然后我们从 deployments.json
文件中读取已部署的 token 地址。CCIP 注册合约将需要这些 token 地址来获取,然后在它们各自的链上注册每个 token 的管理员。
注册 CCIP token 的管理员分两步完成。首先,调用 RegistryModuleOwnerCustom
合约。此合约用于为 token 提议一个新的管理员。当前的管理员,通过我们的 qWHYPE
合约上的 getCCIPAdmin
检索,成为待定管理员。RegistryModuleOwnerCustom
调用 TokenAdminRegistry
合约来提议新的管理员。然后,此状态映射到 TokenAdminRegistry
合约中的 TokenConfig
结构。然后,最后一步,调用 TokenAdminRegistry
合约来接受来自给定 token 的 TokenConfig
中找到的待定管理员的管理角色。
你可能想知道为什么注册 token 的管理员会通过这种复杂的两步过程完成。为什么不直接在 TokenAdminRegistry
合约上注册管理员?这种设计选择有几个原因:
getCCIPOwner
兼容 CCIP 的所有权模型,以及 OpenZeppelin 的 Ownable 式所有权,以及允许加入各种现有 token。此脚本将配置我们的 CCIP token 池,以在彼此之间进行跨链传输。在你的项目目录的 script
文件夹中创建一个名为 ConfigurePools.s.sol
的新文件,并将下面的代码粘贴到其中。
点击展开代码
创建文件:
touch script/ConfigurePools.s.sol
将下面的代码粘贴到文件中:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {TokenAdminRegistry} from "@chainlink/contracts-ccip/contracts/tokenAdminRegistry/TokenAdminRegistry.sol";
import {TokenPool} from "@chainlink/contracts-ccip/contracts/pools/TokenPool.sol";
import {RateLimiter} from "@chainlink/contracts-ccip/contracts/libraries/RateLimiter.sol";
import {Constants} from "./Constants.s.sol";
contract ConfigurePools is Script {
struct Params {
address localToken;
address localPool;
address tokenAdminRegistry;
uint64 remoteSelector;
address remotePool;
address remoteToken;
uint256 pk;
string chainName;
}
string internal constant OUTPUT_PATH = "script/output/deployments.json";
/// forge script script/ConfigurePools.s.sol:ConfigurePools
function run() external {
// Load env vars
bytes memory hyperevm = bytes(vm.envString("HYPEREVM_RPC"));
bytes memory bsc = bytes(vm.envString("BSC_RPC"));
uint256 pk = vm.envUint("PRIVATE_KEY");
require(hyperevm.length != 0, "HYPEREVM_RPC not set");
require(bsc.length != 0, "BSC_RPC not set");
// Load deployed addresses
string memory json = vm.readFile(OUTPUT_PATH);
address tokenHyp = vm.parseJsonAddress(json, ".qWHYPE_hyperevm");
address tokenBsc = vm.parseJsonAddress(json, ".qWHYPE_bsc");
address poolHyp = vm.parseJsonAddress(json, ".qWHYPE_pool_hyperevm");
address poolBsc = vm.parseJsonAddress(json, ".qWHYPE_pool_bsc");
// Preload constants for both chains
Constants constants = new Constants();
Constants.CCIPConstants memory cfgHyp = constants.getCCIPConstants(999);
Constants.CCIPConstants memory cfgBsc = constants.getCCIPConstants(56);
// Forks
uint256 hyperFork = vm.createFork(string(hyperevm));
uint256 bscFork = vm.createFork(string(bsc));
// Configure HyperEVM pool and registry
vm.selectFork(hyperFork);
Params memory pHyp = Params({
localToken: tokenHyp,
localPool: poolHyp,
tokenAdminRegistry: cfgHyp.tokenAdminRegistry,
remoteSelector: cfgBsc.chainSelector,
remotePool: poolBsc,
remoteToken: tokenBsc,
pk: pk,
chainName: "HyperEVM"
});
_setPoolAndApplyChainUpdates(pHyp);
// Configure BSC pool and registry
vm.selectFork(bscFork);
Params memory pBsc = Params({
localToken: tokenBsc,
localPool: poolBsc,
tokenAdminRegistry: cfgBsc.tokenAdminRegistry,
remoteSelector: cfgHyp.chainSelector,
remotePool: poolHyp,
remoteToken: tokenHyp,
pk: pk,
chainName: "BSC"
});
_setPoolAndApplyChainUpdates(pBsc);
}
function _setPoolAndApplyChainUpdates(Params memory p) internal {
// Set pool in TokenAdminRegistry (maps token -> pool)
vm.startBroadcast(p.pk);
TokenAdminRegistry(p.tokenAdminRegistry).setPool(p.localToken, p.localPool);
console.log("setPool set on ", p.localPool);
// Configure pool with remote chain, pool, rate limits
TokenPool pool = TokenPool(p.localPool);
TokenPool.ChainUpdate[] memory chainUpdates = new TokenPool.ChainUpdate[](1);
// Encode remote pool addresses (single entry)
bytes[] memory remotePoolAddressesEncoded = new bytes[](1);
remotePoolAddressesEncoded[0] = abi.encode(p.remotePool);
chainUpdates[0] = TokenPool.ChainUpdate({
remoteChainSelector: p.remoteSelector,
remotePoolAddresses: remotePoolAddressesEncoded,
remoteTokenAddress: abi.encode(p.remoteToken),
outboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0}),
inboundRateLimiterConfig: RateLimiter.Config({isEnabled: false, capacity: 0, rate: 0})
});
// No removals, apply chain updates
uint64[] memory chainSelectorRemovals = new uint64[](0);
pool.applyChainUpdates(chainSelectorRemovals, chainUpdates);
console.log("Chain update applied to pool at address:");
console.log(p.localPool);
vm.stopBroadcast();
}
}
在这个脚本中,我们首先加载 RPC URL 和私钥的环境变量。然后我们从 deployments.json
文件中读取已部署的 token 和池地址。我们使用 Constants
合约为 HyperEVM 和 BSC 获取必要的 CCIP 常量。这些常量包括 TokenAdminRegistry
地址和链选择器,这是为每个 token 设置池所需的。
我们定义了一个 Params
结构来保存配置每个池所需的所有参数。它不仅更具可读性,而且还有助于避免 Solidity 中的“堆栈太深”错误。我们调用 TokenAdminRegistry
合约上的 setPool
函数,将每个 token 映射到其各自的池。接下来,我们使用 TokenPool
合约上的 applyChainUpdates
函数,使用远程链选择器、远程池地址和远程 token 地址配置每个池。在此示例中,我们为了简单起见禁用了速率限制,但在生产环境中,你可以根据需要配置适当的速率限制。
此脚本将在 qWHYPE 合约上存入少量 HYPE,以铸造 qWHYPE token,然后执行到 BSC 的跨链传输。在你的项目目录的 script
文件夹中创建一个名为 DepositAndTransferTokens.s.sol
的新文件,并将下面的代码粘贴到其中。
点击展开代码
创建文件:
touch script/DepositAndTransferTokens.s.sol
将下面的代码粘贴到文件中:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {IERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {Constants} from "./Constants.s.sol";
import {qWHYPE} from "../src/qWHYPE.sol";
contract DepositAndTransferTokens is Script {
string internal constant OUTPUT_PATH = "script/output/deployments.json";
/// forge script script/DepositAndTransferTokens.s.sol:DepositAndTransferTokens
function run() external {
// Load env vars
bytes memory hyperevm = bytes(vm.envString("HYPEREVM_RPC"));
bytes memory bsc = bytes(vm.envString("BSC_RPC"));
uint256 pk = vm.envUint("PRIVATE_KEY");
require(hyperevm.length != 0, "HYPEREVM_RPC not set");
require(bsc.length != 0, "BSC_RPC not set");
// Read deployed token addresses
string memory json = vm.readFile(OUTPUT_PATH);
address tokenHyp = vm.parseJsonAddress(json, ".qWHYPE_hyperevm");
// Create forks and select HyperEVM (source)
uint256 hyperFork = vm.createFork(string(hyperevm));
vm.selectFork(hyperFork);
// Resolve CCIP constants for source and destination
Constants constants = new Constants();
Constants.CCIPConstants memory cfgHyp = constants.getCCIPConstants(999);
Constants.CCIPConstants memory cfgBsc = constants.getCCIPConstants(56);
address router = cfgHyp.router;
uint64 destinationChainSelector = cfgBsc.chainSelector;
address sender = vm.addr(pk);
// Amount to wrap and transfer: 0.01 HYPE -> 0.01 qWHYPE (18 decimals)
uint256 amount = 0.01 ether;
vm.startBroadcast(pk);
// Deposit HYPE to mint qWHYPE on HyperEVM
qWHYPE(tokenHyp).deposit{value: amount}();
console.log("Deposited and minted qWHYPE amount");
console.log(amount);
// Approve router to spend qWHYPE
IERC20(tokenHyp).approve(router, amount);
console.log("Approved router to spend qWHYPE");
// Build CCIP EVM2AnyMessage for token transfer
Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1);
tokenAmounts[0] = Client.EVMTokenAmount({token: tokenHyp, amount: amount});
Client.EVMExtraArgsV1 memory extraArgs = Client.EVMExtraArgsV1({gasLimit: 0});
bytes memory extraArgsBytes = Client._argsToBytes(extraArgs);
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(sender),
data: abi.encode(),
tokenAmounts: tokenAmounts,
feeToken: address(0), // pay fees in native
extraArgs: extraArgsBytes
});
// Route via CCIP using native token for gas
IRouterClient routerClient = IRouterClient(router);
require(routerClient.isChainSupported(destinationChainSelector), "Dest chain not supported");
uint256 fee = routerClient.getFee(destinationChainSelector, message);
console.log("Estimated fee (native)", fee);
bytes32 messageId = routerClient.ccipSend{value: fee}(destinationChainSelector, message);
console.log("CCIP messageId");
console.logBytes32(messageId);
vm.stopBroadcast();
}
}
在这个脚本中,我们首先加载 RPC URL 和私钥的环境变量。然后我们从 deployments.json
文件中读取已部署的 token 地址。我们创建一个 HyperEVM 链的 fork,它将成为我们跨链传输的源链。
我们使用 Constants
合约为 HyperEVM 和 BSC 获取必要的 CCIP 常量。这些常量包括 Router
地址和链选择器,这是发送跨链消息所需的。我们定义要存入和传输的 HYPE 数量,在本例中为 0.01 HYPE。
我们调用 qWHYPE
合约上的 deposit
函数,通过存入 HYPE 来铸造 qWHYPE token。接下来,我们批准 Router 花费我们的 qWHYPE token。然后,我们构建一个 Client.EVM2AnyMessage
结构,其中包含我们的跨链传输的详细信息,包括接收者地址、token 数量和任何额外的参数。
最后,我们调用 Router 合约上的 ccipSend
函数来发送跨链消息,以源链(HYPE)的本地 token 支付所需的费用。该脚本记录估计的费用和 CCIP 传输的消息 ID。此消息 ID 可用于在 Chainlink CCIP Explorer 上跟踪传输的状态。
此脚本将执行从任一方向跨链传输 qWHYPE token。在你的项目目录的 script
文件夹中创建一个名为 TransferTokens.s.sol
的新文件,并将下面的代码粘贴到其中。
点击展开代码
创建文件:
touch script/TransferTokens.s.sol
将下面的代码粘贴到文件中:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {Script, console} from "forge-std/Script.sol";
import {IERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";
import {IRouterClient} from "@chainlink/contracts-ccip/contracts/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/contracts/libraries/Client.sol";
import {Constants} from "./Constants.s.sol";
// Transfer-only script: does NOT deposit. Assumes caller holds qWHYPE on source chain.
// 仅传输脚本:不存款。 假设调用者在源链上持有 qWHYPE。
contract TransferTokens is Script {
string internal constant OUTPUT_PATH = "script/output/deployments.json";
uint256 internal constant AMOUNT = 0.01 ether;
/// forge script script/TransferTokens.s.sol:TransferTokens --sig 'run(string)' <to-bsc | to-hyperevm>
function run(string memory to) external {
bool toBsc = _eq(to, "to-bsc");
require(toBsc || _eq(to, "to-hyperevm"), "to must be 'to-bsc' or 'to-hyperevm'");
_run(toBsc);
}
function run() external {
string memory to = vm.envOr("TO", string(""));
require(bytes(to).length != 0, "Set TO or use run(string)");
bool toBsc = _eq(to, "to-bsc");
require(toBsc || _eq(to, "to-hyperevm"), "to must be 'to-bsc' or 'to-hyperevm'");
_run(toBsc);
}
function _run(bool toBsc) internal {
// Validate RPCs and select source fork
// 验证 RPC 并选择源 fork
require(bytes(vm.envString("HYPEREVM_RPC")).length != 0, "HYPEREVM_RPC not set");
require(bytes(vm.envString("BSC_RPC")).length != 0, "BSC_RPC not set");
vm.selectFork(vm.createFork(toBsc ? vm.envString("HYPEREVM_RPC") : vm.envString("BSC_RPC")));
// Resolve router and destination selector
// 解析路由器和目标选择器
address router = (new Constants()).getCCIPConstants(block.chainid).router;
uint64 destSelector = (new Constants()).getCCIPConstants(toBsc ? 56 : 999).chainSelector;
// Source token address
// 源 token 地址
address srcToken = toBsc
? vm.parseJsonAddress(vm.readFile(OUTPUT_PATH), ".qWHYPE_hyperevm")
: vm.parseJsonAddress(vm.readFile(OUTPUT_PATH), ".qWHYPE_bsc");
// Start broadcasting
// 开始广播
vm.startBroadcast(vm.envUint("PRIVATE_KEY"));
// Approve router to spend qWHYPE
// 批准路由器花费 qWHYPE
IERC20(srcToken).approve(router, AMOUNT);
console.log("Approved router to spend qWHYPE");
// Build token amounts
// 构建 token 数量
Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1);
tokenAmounts[0] = Client.EVMTokenAmount({token: srcToken, amount: AMOUNT});
// Send via CCIP using native gas as fee
// 使用本地 gas 作为费用通过 CCIP 发送
require(IRouterClient(router).isChainSupported(destSelector), "Dest chain not supported");
uint256 fee = IRouterClient(router).getFee(
destSelector,
Client.EVM2AnyMessage({
receiver: abi.encode(vm.addr(vm.envUint("PRIVATE_KEY"))),
data: abi.encode(),
tokenAmounts: tokenAmounts,
feeToken: address(0),
extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0}))
})
);
console.log("Estimated fee (native)");
console.log(fee);
console.log("CCIP messageId");
console.logBytes32(
IRouterClient(router).ccipSend{value: fee}(
destSelector,
Client.EVM2AnyMessage({
receiver: abi.encode(vm.addr(vm.envUint("PRIVATE_KEY"))),
data: abi.encode(),
tokenAmounts: tokenAmounts,
feeToken: address(0),
extraArgs: Client._argsToBytes(Client.EVMExtraArgsV1({gasLimit: 0}))
})
)
);
vm.stopBroadcast();
}
function _eq(string memory a, string memory b) internal pure returns (bool) {
return keccak256(bytes(a)) == keccak256(bytes(b));
}
}
在这个脚本中,我们首先检查在终端中传递的参数是 to-bsc
还是 to-hyperevm
,指示传输的方向。基于此,我们创建适当的源链(HyperEVM 或 BSC)的 fork。我们使用 Constants
合约为两条链获取必要的 CCIP 常量。这些常量包括 Router
地址和链选择器,这是发送跨链消息所需的。
我们定义要传输的 qWHYPE 的常量数量,在本例中为 0.01 qWHYPE。我们批准 Router 花费我们的 qWHYPE token,并构建一个 Client.EVM2AnyMessage
结构,其中包含我们的跨链传输的详细信息,包括接收者地址、token 数量和任何额外的参数。
最后,我们调用 Router 合约上的 ccipSend
函数来发送跨链消息,以源链(HYPE 或 BNB)的本地 token 支付所需的费用。该脚本记录估计的费用和 CCIP 传输的消息 ID。此消息 ID 可用于在 Chainlink CCIP Explorer 上跟踪传输的状态。
太棒了!我们现在已经编写了所有必要的脚本来部署和配置我们的 CCIP token 池,并在 HyperEVM 和 BSC 之间执行 qWHYPE token 的跨链传输。我们现在将继续在下一节中执行这些脚本。
现在我们已经编写了所有必要的脚本,我们可以按顺序执行它们,以部署和配置我们的 CCIP 代币池,并执行 qWHYPE 代币的跨链转移。
早些时候,我们讨论了 Hyperliquid 的多区块架构,较小的区块最多使用 2M gas,而较大的区块可以使用高达 30M gas。我们的合约部署交易肯定会超过 2M gas 限制,因此我们需要首先启用我们的帐户以使用更大的区块。为此,请在你的终端中执行以下命令:
source .env
npx @layerzerolabs/hyperliquid-composer set-block --size big --network mainnet --private-key $PRIVATE_KEY
信息
我们建议 不要 直接在命令行中传递你的私钥,因为它可能会存储在你的 shell 历史记录中。始终使用环境变量或安全方法来处理敏感信息!
警告
如果你的钱包未在 HyperCore 上注册,则此命令可能会失败,并显示 User or API Wallet does not exist.
。 确保你已在 Hyperliquid 上进行存款或交易以注册你的钱包。如果你正确地按照 从 HyperCore 桥接到 HyperEVM 中的步骤操作,你的钱包已经注册!
太棒了!我们的账户现在可以使用更大的区块了。 现在,我们将使用 DeployTokens.s.sol
脚本部署我们的代币:
forge script script/DeployTokens.s.sol:DeployTokens --broadcast --verify --verifier etherscan
此命令会将我们的代币部署到 HyperEVM 和 BSC 网络。你将在终端中看到部署进度,并在交易确认后看到交易哈希。--broadcast
标志表示我们要将交易发送到网络,而 --verify
和 --verifier etherscan
标志将在部署后自动在 hyperevmscan.io 和 bscscan.com 上验证我们的合约。你还会注意到你的 deployments.json
文件已填充已部署的合约地址。你的终端输出应如下所示:
接下来,我们将使用 DeployPools.s.sol
脚本部署我们的 CCIP 代币池:
forge script script/DeployPools.s.sol:DeployPools --broadcast --verify --verifier etherscan
此命令会将我们的代币池部署到 HyperEVM 和 BSC 网络。 与上一步类似,你将在终端中看到部署进度,并在交易确认后看到交易哈希。deployments.json
文件将使用已部署的池地址进行更新。你的终端输出应如下所示:
接下来,我们将使用 SetupAdmin.s.sol
脚本设置我们代币的管理员,并使用 ConfigurePools.s.sol
脚本配置我们的池。我们可以使用以下命令按顺序执行这两个脚本:
forge script script/SetupAdmin.s.sol:SetupAdmin --broadcast
forge script script/ConfigurePools.s.sol:ConfigurePools --broadcast
与上一步类似,一旦交易被确认,你将看到类似的终端输出,其中包含交易哈希。
最后,我们将使用 DepositAndTransferTokens.s.sol
脚本执行从 HyperEVM 到 BSC 的 qWHYPE 代币的跨链转移:
forge script script/DepositAndTransferTokens.s.sol:DepositAndTransferTokens --broadcast
此命令会将 HYPE 存入以在 HyperEVM 上铸造 qWHYPE,然后将 qWHYPE 代币转账到 BSC。你将在终端中看到转移进度,并在交易确认后看到交易哈希。你还将看到转移的 CCIP 消息 ID,你可以使用该 ID 在 Chainlink CCIP Explorer 上跟踪转移的状态。你的终端将输出消息 ID,如下所示:
终端输出显示 CCIP 消息 ID
CCIP Explorer 显示跨链转移的状态
HyperEVM Explorer 显示转移交易的详细信息
BSC Explorer 显示收到的转移交易的详细信息
让我们验证是否已在 BSC 上收到 qWHYPE
代币。打开你的 MetaMask 钱包,切换到 BSC 网络,然后使用在 deployments.json
文件中找到的带有 qWHYPE_bsc
键的地址导入 qWHYPE
代币。如果你需要导入代币的帮助,请按照 MetaMask 的 此处 指南进行操作。你应该在你的钱包中看到 0.01 qWHYPE 代币的余额,确认跨链转移已成功!
TransferTokens.s.sol
脚本可用于在 HyperEVM 和 BSC 之间以任一方向转移 qWHYPE 代币。你可以通过在运行脚本时传递 to-bsc
或 to-hyperevm
作为参数来指定转移的方向。例如,要将 qWHYPE 代币从 BSC 转移到 HyperEVM,你可以运行以下命令:
forge script script/TransferTokens.s.sol:TransferTokens --broadcast --sig 'run(string)' 'to-hyperevm'
输出将类似于我们的首次跨链转移,显示交易哈希和 CCIP 消息 ID。现在,将消息 ID 粘贴到 Chainlink CCIP Explorer 中将显示相反方向的转移:
恭喜!你已成功学习如何使用 Chainlink CCIP 在 Hyperliquid 上桥接代币。你已经部署了自己的 ERC-20 代币,设置了 CCIP 代币池,并执行了 qWHYPE 代币在 HyperEVM 和 BSC 之间的跨链转移。
现在你有了一个可用的跨链桥,你可以进一步改进和自定义你的设置。以下是一些想法:
ConfigurePools.s.sol
脚本中配置速率限制。如果你遇到问题或有疑问,请在我们的 Discord 中提出。通过在 X (@QuickNode) 或我们的 Telegram 公告频道 上关注我们,及时了解最新信息。
如果你对此文档有任何反馈或问题,请 告诉我们。我们很乐意听取你的意见!
- 原文链接: quicknode.com/guides/oth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!