本文介绍了如何使用 Chainlink CCIP 在 Hyperliquid 上搭建跨链桥,包括创建 HIP-1 资产的包装合约、部署 CCIP 代币池、配置管理角色,并在 HyperEVM 和 BSC 之间执行跨链传输。通过 Foundry 脚本完成合约部署、配置和交互,最终实现 HYPE 代币在 HyperEVM 和 BSC 之间的跨链转移。
Hyperliquid 将两层结构置于一个统一的状态之下:HyperCore,一个通过签名操作访问的超低延迟订单簿引擎;以及 HyperEVM,一个完全兼容 EVM 的网络,用于标准的 Solidity 开发。
Chainlink CCIP 允许使用 销毁并铸造 (burn-and-mint) 机制在链之间桥接资产。在 Hyperliquid 中,HyperCore(HIP-1)资产与其 HyperEVM 表示之间的转换由原生协议流程处理。
在本指南中,你将使用 Foundry 封装一个对应于 HIP-1 资产的 HyperEVM 代币,并使用 CCIP 在 HyperEVM 和 BNB 智能链(BSC)之间桥接该代币。
免责声明
在本指南中,我们使用的是每个链的 主网 (MAINNET)。这是因为在撰写本文时,Chainlink CCIP 在 HyperEVM 测试网上不可用。请谨慎操作,只使用你能承受损失的资金。
我们将使用 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 代币池合约。第二个包包含符合 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 代币表示形式。在本指南中,我们将使用 Hyperliquid 的原生生态系统代币:HYPE。转到 Hyperliquid 交易界面 并使用 MetaMask 登录以开始。
如果你还没有 HYPE 代币,你可以通过将 Arbitrum Mainnet 中的 USDC 存入 Hyperliquid,然后在交易界面上的 现货 (Spot) 上将其交易为 HYPE 来获取一些 HYPE 代币:
提醒
在本指南中,我们使用的是每个链的 主网 (Mainnet)。这是因为在撰写本文时,Chainlink CCIP 在 HyperEVM 测试网上不可用。请谨慎操作,只使用你能承受损失的资金。
一旦你有了 HYPE,你就可以继续将其桥接到 HyperEVM。为此,请单击图表下方页面底部的 转移到/从 EVM (Transfer to/from EVM) 按钮。只需输入你要桥接的 HYPE 数量,然后单击 确认 (Confirm)。
将 HYPE 桥接到 HyperEVM
太棒了!你已成功将 HYPE 从 HyperCore 桥接到 HyperEVM。你现在应该能够在 HyperEVM 网络上的 MetaMask 中看到你的 HYPE 余额。我们现在将继续为 HYPE 创建一个符合 Chainlink 跨链传输要求的封装合约。
要使用 Chainlink CCIP 在 HyperEVM 和 BSC 之间桥接 HYPE,我们需要创建一个符合 Chainlink 跨链传输要求的封装合约。这涉及创建一个新的 ERC20 代币合约,该合约封装 HyperEVM 上现有的 HYPE 资产,以及 BSC 上相应的 ERC20 代币合约。
我们将创建一个符号为 qWHYPE 的 QuickNode 封装 Hype (QuickNode Wrapped Hype) 代币。输入以下命令以在项目目录的 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);
}
}
让我们分解 qWHYPE.sol
合约中的代码。该合约从 @chainlink/contracts
包中导入 BurnMintERC20
合约,该合约提供了铸造和销毁代币所需的必要功能。当用户将 HYPE 存入合约时,它会向他们的地址铸造等量的 qWHYPE 代币。相反,当用户提取 qWHYPE 代币时,合约会销毁代币并将相应数量的 HYPE 转回用户的地址。
这个特定的合约将部署在 HyperEVM 网络上。在 BSC 上,我们将直接部署基础 BurnMintERC20
合约,因为它不需要任何其他功能。我们将在下一节中使用脚本部署两个合约。
我们将编写几个 Foundry 脚本来部署和与我们的智能合约交互。本节将分为几个部分,以详细介绍每个脚本。我们将创建以下脚本并按此顺序执行它们:
脚本名称 | 描述 |
---|---|
DeployTokens.s.sol |
在 HyperEVM 上部署 qWHYPE 合约,并在 BSC 上部署基础 BurnMintERC20 合约 |
DeployPools.s.sol |
在 HyperEVM 和 BSC 上部署 CCIP 代币池合约 |
SetupAdmin.s.sol |
在每个链的 CCIP 合约上注册和配置管理角色 |
ConfigurePools.s.sol |
配置我们的 CCIP 代币池,以便在彼此之间进行跨链传输 |
DepositAndTransferTokens.s.sol |
在 qWHYPE 合约上存入少量 HYPE 以铸造 qWHYPE 代币,然后执行到 BSC 的跨链传输 |
TransferTokens.s.sol |
执行任何方向 qWHYPE 代币的跨链传输 |
在编写脚本之前,我们需要一种方法来跟踪我们部署的合约地址,以及一个文件来存储我们将在此脚本中使用的常见常量。
执行以下命令来创建适当的文件:
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 {
// 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");
// print deployer address
address deployer = vm.addr(pk);
console.log("Deployer address:", deployer);
// Deploy on both chains
address hyperAddr = deployOn(hyperevm, deployer, pk);
address bscAddr = deployOn(bsc, deployer, pk);
// Write deployed addresses to JSON file
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("\nDeployed qWHYPE to:", tokenAddr, "on", chainName);
BurnMintERC20(tokenAddr).grantMintAndBurnRoles(deployer);
console.log("Granted minter and burner roles on", chainName, "qWHYPE to:", 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,开始使用提供的私钥广播交易,并在 HyperEVM 上部署 qWHYPE
合约,或者在 BSC 上部署基础的 BurnMintERC20
合约。部署后,它会授予部署者地址在已部署的 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>
// / 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 显示收到的转移交易的详细信息
让我们验证一下 qWHYPE
代币是否已在 BSC 上收到。打开你的 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 代币池,并执行了 HyperEVM 和 BSC 之间 qWHYPE 代币的跨链转移。
现在你有了一个有效的跨链桥,你可以进一步改进和自定义你的设置。以下是一些想法:
ConfigurePools.s.sol
脚本中配置速率限制。如果你遇到困难或有疑问,请将它们放在我们的 Discord 中。通过在 X (@QuickNode) 或我们的 Telegram 公告频道 上关注我们,了解最新信息。
如果你对此文档有任何反馈或疑问,请 告诉我们。我们很乐意听取你的意见!
- 原文链接: quicknode.com/guides/hyp...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!