閱讀時間 38 分鐘
概覽
Hyperliquid 將兩層架構整合於單一統一狀態之下:HyperCore 是一款透過簽名操作存取的超低延遲訂單簿引擎;HyperEVM 則是一個完全相容於 EVM 的網路,可用於標準 Solidity 開發。
Chainlink CCIP 透過「銷毀與鑄造」機制,實現跨鏈資產橋接。在 Hyperliquid 系統中,HyperCore (HIP-1) 資產與其 HyperEVM 表示形式之間的轉換,由原生協議流程處理。
在本指南中,您將使用 Foundry 對應於 HIP-1 資產的 HyperEVM 代幣進行封裝,並透過 CCIP 將該代幣在 HyperEVM 與 BNB Smart Chain (BSC) 之間進行跨鏈轉移。
您將負責的工作內容
- 為與 HIP-1 資產綁定的 HyperEVM 代幣建立一個封裝合約
- 在 HyperEVM 和 BSC 上部署並配置 CCIP 代幣池
- 在鏈上驗證我們已部署的代幣與流動性池合約
- 使用 Foundry 腳本在 HyperEVM 與 BSC 之間執行 CCIP 轉帳
您需要準備的物品
- 已安裝Node.js v22 以上版本及套件管理工具(npm、yarn 或 pnpm)
- MetaMask已新增 HyperEVM 主網和 Binance Smart Chain 主網
- HyperEVM和Binance Smart Chain的 Quicknode 端點
- 具備Solidity和Foundry的相關經驗
本指南中,我們使用的是各條鏈的主網(MAINNETS)。這是因為在撰寫本文時,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="您的_hyperevm_rpc_網址"
BSC_RPC="您的 BSC RPC 網址"
PRIVATE_KEY="您的私鑰"
ETHERSCAN_API_KEY="您的_etherscan_api_key"
我們稍後將討論這些變數各自代表什麼,以及如何取得它們。下一步是刪除預設值 src/Counter.sol 合約及 script/Counter.s.sol Foundry 為我們生成的腳本。本指南中將不會使用這些檔案。此外,您也可以在終端機中執行以下指令來刪除這些檔案:
rm src/Counter.sol
rm script/Counter.s.sol
您還需要設定 foundry.toml 請在專案資料夾的根目錄中建立一個檔案,用以包含 Solidity 編譯器的關鍵設定,以及我們即將使用的 import 語句的重新映射。以下是您的 foundry.toml 檔案應如下所示:
[個人檔案.預設]
src = "src"
out = "out"
庫 = ["lib"]
優化器 = true
optimizer_runs = 200
重新映射 = [
'@chainlink/contracts-ccip/=node_modules/@chainlink/contracts-ccip/',
'@chainlink/contracts/=node_modules/@chainlink/contracts/',
]
fs_permissions = [{ 存取權限 = "讀寫", 路徑 = "./" }]
在此檔案中,我們新增了 優化器 設定,以優化我們的智慧合約以便部署。該 fs_permissions 此設定項可讓 Foundry 讀取及寫入專案目錄中的檔案。該 重新映射 這些項目是本指南中將要使用的 Chainlink 合約套件的路徑。接下來我們將安裝這些套件。
安裝依賴項
在本指南中,我們將使用兩個 Chainlink 合約套件: @chainlink/contracts-ccip 以及 @chainlink/contracts. 第一個套件包含我們將與之互動的核心 CCIP 合約,以及我們即將部署的 CCIP 代幣池合約。第二個套件則包含量身訂製的 ERC20 符合 Chainlink 跨鏈轉帳要求的合約。
我們還將安裝 @layerzerolabs/hyperliquid-composer 套件,該套件將用於提高我們在 HyperEVM 上進行交易的 Gas 限制。HyperEVM 具有一個 多區塊架構 該系統由兩類區塊組成:較小區塊的 gas 限制為 200 萬 gas 單位,較大區塊的 gas 限制則為 3,000 萬 gas 單位。較小區塊的區塊時間為 1 秒,而較大區塊的區塊時間為 1 分鐘。預設情況下,HyperEVM 上的帳戶僅能發送符合較小區塊 gas 限制的交易。此套件將使我們的帳戶能夠發送具有更高 gas 限制的交易,以適應較大區塊的容量。
要安裝這些套件,請使用您偏好的套件管理工具輸入以下指令:
- npm
- 紗線
npm install @chainlink/contracts-ccip @chainlink/contracts @layerzerolabs/hyperliquid-composer
yarn add @chainlink/contracts-ccip @chainlink/contracts @layerzerolabs/hyperliquid-composer
太好了!您已成功安裝本指南中將使用的依賴項。接下來,我們將討論需要設定在我們的 .env 檔案。
環境變數
Quicknode 端點
首先,我們需要取得 HyperEVM 和 BSC 的適當 RPC 端點。您可以從以下位置取得這些資訊: Quicknode. 只需註冊免費試用,建立一個新的多鏈端點,並複製各條鏈的 HTTPS 網址。將對應的網址貼上至 HYPEREVM_RPC 以及 BSC_RPC 您在其中的變數 .env 檔案。
Etherscan API
接著,請前往您的個人檔案頁面 Etherscan 然後切換至「API 金鑰」分頁。若您尚未擁有帳戶,請建立一個 這裡. 在此處,您將建立一個 API 金鑰,該金鑰將協助您在鏈上驗證您的智慧合約。請複製該 API 金鑰,並將其貼上至 ETHERSCAN_API_KEY 您在其中的變數 .env 檔案。
私鑰
最後,請開啟您的 MetaMask,並複製其中一個帳戶的私鑰。若想了解如何取得您的私鑰,請參閱這篇簡短的 指南. 將此私鑰貼上至 PRIVATE_KEY 您在其中的變數 .env 檔案。
至此,您中的所有環境變數 .env 該檔案現在應該已填入資料。此設定流程的最後一步,是將適當的網路新增至我們的 MetaMask 錢包中。
將網路新增至 MetaMask
我們即將將 HyperEVM 主網和 Binance Smart Chain 主網加入我們的 MetaMask 錢包中。加入這些網路的最簡單方法是前往 hyperevmscan.io 以及 bscscan.com 然後點擊 新增 頁面左下角的按鈕。這將把這些網路新增至您的 MetaMask 錢包:

恭喜!您已成功為本指南設定好開發環境。下一步,我們將把 HyperCore 中的 HIP-1 資產橋接至 HyperEVM。
從 HyperCore 銜接至 HyperEVM
您現在將把一項 HIP-1 資產從 HyperCore 橋接至 HyperEVM。此過程涉及將 HIP-1 資產轉換為其對應的 HyperEVM 代幣形式。在本指南中,我們將使用 Hyperliquid 的原生生態系統代幣:HYPE。請前往Hyperliquid 交易介面,並使用MetaMask登入以開始操作。
如果您目前尚未持有 HYPE 代幣,您可以透過將 USDC 從 Arbitrum 主網存入 Hyperliquid,然後在交易介面的現貨市場將其兌換為 HYPE 來取得:

一旦您持有一定數量的 HYPE,即可將其橋接至 HyperEVM。操作時,請點擊頁面底部(圖表下方)的「轉入/轉出 EVM」按鈕。只需輸入您要橋接的 HYPE 數量,然後點擊「確認」即可。

將 HYPE 與 HyperEVM 串聯起來
太棒了!您已成功將 HYPE 從 HyperCore 橋接至 HyperEVM。現在,您應該可以在 MetaMask 的 HyperEVM 網路中查看您的 HYPE 餘額。接下來,我們將著手為 HYPE 建立一個符合 Chainlink 跨鏈轉帳要求的封裝合約。
為 HYPE 建立封裝合約
為了透過 Chainlink CCIP 在 HyperEVM 與 BSC 之間進行 HYPE 的跨鏈轉移,我們需要建立一個符合 Chainlink 跨鏈轉移要求的包裝合約。這涉及建立一個新的 ERC20 代幣合約,用以包裝 HyperEVM 上的現有 HYPE 資產,並在 BSC 上建立一個對應的 ERC20 代幣合約。
我們將建立一個 Quicknode 炒作風潮 符號為 的代幣 qWHYPE. 輸入以下指令,在 src 專案目錄中的資料夾,並將以下程式碼貼入其中。
- 建立檔案
- 程式碼
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 合約。該合約導入了 BurnMintERC20 來自該的合約 @chainlink/contracts 該套件提供了鑄造和銷毀代幣所需的必要功能。當使用者將 HYPE 存入合約時,合約會向其地址鑄造等值的 qWHYPE 代幣。反之,當使用者提取 qWHYPE 代幣時,合約會銷毀這些代幣,並將相應數量的 HYPE 轉回使用者的地址。
這份特定合約將部署於 HyperEVM 網路。在 BSC 上,我們將部署基礎版 BurnMintERC20 直接部署該合約,因為它不需要任何額外功能。我們將在下一節中透過腳本部署這兩份合約。
撰寫鑄字腳本
我們將編寫幾個 Foundry 腳本,用來部署智慧合約並與其互動。本節將分為幾個部分,詳細介紹每個腳本。我們將依以下順序建立並執行這些腳本:
| 腳本名稱 | 說明 |
|---|---|
DeployTokens.s.sol | 將 qWHYPE 合約部署至 HyperEVM 及其基礎鏈上 BurnMintERC20 BSC 上的合約 |
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");
}
}
腳本選單
- DeployTokens.s.sol
- DeployPools.s.sol
- SetupAdmin.s.sol
- ConfigurePools.s.sol
- DepositAndTransferTokens.s.sol
- TransferTokens.s.sol
此腳本將部署 qWHYPE HyperEVM 上的合約及其基礎 BurnMintERC20 BSC 上的合約。在 劇本 您專案目錄中名為 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");
}
}
了解 DeployTokens 腳本
在此腳本中,我們首先載入 RPC URL 和私密金鑰的環境變數。接著,我們定義一個 deployOn 一個以 RPC URL、部署者地址及私鑰作為參數的函式。此函式會建立指定區塊鏈的分叉,使用提供的私鑰開始廣播交易,並部署 qWHYPE 合約(在 HyperEVM 上)或基礎合約 BurnMintERC20 合約(在 BSC 上)。部署完成後,該合約會授予部署者地址在已部署的代幣合約上進行鑄造與銷毀的權限。
此腳本將在 HyperEVM 和 BSC 上部署 CCIP 代幣池合約。請在 劇本 您專案目錄中名為 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);
}
}
了解 DeployPools 腳本
在此腳本中,我們首先載入 RPC URL 和私密金鑰的環境變數。接著,我們從 deployments.json 在前一個腳本中建立的檔案。我們使用 常數 合約。
這些常數包含 Chainlink 在各鏈上部署的核心 CCIP 合約,包括Router、TokenAdminRegistry、RegistryModuleOwnerCustom,以及池部署所需的RiskManagementNetwork (RMN)代理地址。 Router 負責路由跨鏈訊息;TokenAdminRegistry 管理代幣地址與其對應池之間的映射關係;RegistryModuleOwnerCustom 用於不同所有權模式下的代幣管理員註冊;而 RMN 代理則用於驗證跨鏈訊息的 RMN 簽名。
利用這些常數,我們部署了 BurnMintTokenPool 兩條鏈上的合約,賦予它們在各自代幣合約中鑄造與銷毀的權限。最後,我們更新 deployments.json 包含已部署代幣池位址的檔案。
此腳本將為我們在各鏈上的 CCIP 註冊合約中部署的代幣,註冊其管理員。請在 劇本 您專案目錄中名為 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";
}
}
了解 SetupAdmin 腳本
在此腳本中,我們首先載入 RPC URL 和私密金鑰的環境變數。接著,我們從 deployments.json 檔案。CCIP 註冊合約將需要這些代幣地址,以便從各條鏈上擷取並註冊各代幣的管理員。
註冊 CCIP 代幣的管理員需分兩步驟進行。首先, RegistryModuleOwnerCustom 合約被呼叫。此合約用於提名該代幣的新管理員。目前的管理員是透過 getCCIPAdmin 在我們的 qWHYPE 合約,即成為待處理的管理員。該 RegistryModuleOwnerCustom 稱為 TokenAdminRegistry 合約以提出新的管理員。此狀態隨後會映射至一個 TokenConfig struct 在 TokenAdminRegistry 合約。接著是最後一步,也就是 TokenAdminRegistry 呼叫此函式以接受從 TokenConfig 針對所給的代幣。
您可能會好奇,為何要透過這個複雜的兩步驟流程來註冊代幣的管理員。為何不直接在 TokenAdminRegistry 縮寫?做出此設計選擇有幾個原因:
- 強化安全性:透過兩步驟審核流程,現任管理員可在新管理員任命正式生效前,先進行審查並批准。
- 多簽名相容性:透過先提出管理員變更提案,再予以接受,可與多簽名錢包(例如 Safe)相容。
- 所有權模式:透過以下方式與 CCIP 的所有權模式相容
getCCIPOwner此外,透過 OpenZeppelin 的 Ownable 風格所有權機制,以及其他功能,可支援整合多種現有代幣。
此腳本將配置我們的 CCIP 代幣池,以便它們之間進行跨鏈轉帳。請在 劇本 您專案目錄中名為 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();
}
}
了解 ConfigurePools 腳本
在此腳本中,我們首先載入 RPC URL 和私鑰的環境變數。接著,我們從 deployments.json 檔案。我們使用 常數 合約。這些常數包括 TokenAdminRegistry 設定每個代幣的池時所需的地址和鏈選擇器。
我們定義了一個 參數 用於存放配置每個池所需的所有參數的結構體。這不僅更易於閱讀,還能避免在 Solidity 中發生「堆疊過深」的錯誤。我們將此結構體稱為 setPool 在...上的函式 TokenAdminRegistry 合約會將每個代幣映射至其對應的池。接著,我們使用 applyChainUpdates 在...上的函式 TokenPool 合約。在此範例中,為簡化起見,我們停用了速率限制;但在生產環境中,您可以根據需求設定適當的速率限制。
此腳本將向 qWHYPE 合約存入少量 HYPE,以鑄造 qWHYPE 代幣,然後執行跨鏈轉帳至 BSC。請在 劇本 您專案目錄中名為 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();
}
}
了解 DepositAndTransferTokens 腳本
在此腳本中,我們首先載入 RPC URL 和私密金鑰的環境變數。接著,我們從 deployments.json 檔案。我們建立 HyperEVM 鏈的分叉,這將作為我們進行跨鏈轉帳的來源鏈。
我們使用 常數 合約。這些常數包括 路由器 傳送跨鏈訊息所需的地址與鏈選取器。我們定義要存入並轉帳的 HYPE 數量,本例中為 0.01 HYPE。
我們稱這個為 押金 在...上的函式 qWHYPE 透過存入 HYPE 來鑄造 qWHYPE 代幣的合約。接著,我們授權 Router 動用我們的 qWHYPE 代幣。然後,我們建立一個 Client.EVM2AnyMessage 一個結構體,其中包含我們的跨鏈轉帳詳細資訊,包括收款人地址、代幣數量以及任何額外參數。
最後,我們呼叫該 ccipSend 在 Router 合約中呼叫函式以傳送跨鏈訊息,並以來源鏈的原生代幣(HYPE)支付所需手續費。該腳本會記錄預估手續費以及 CCIP 轉帳的訊息 ID。此訊息 ID 可用於在 Chainlink CCIP 探索器.
此腳本將執行 qWHYPE 代幣的跨鏈轉帳,無論是從哪個方向皆可。請在 劇本 您專案目錄中名為 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.
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
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
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
IERC20(srcToken).approve(router, AMOUNT);
console.log("Approved router to spend qWHYPE");
// Build token amounts
Client.EVMTokenAmount[] memory tokenAmounts = new Client.EVMTokenAmount[](1);
tokenAmounts[0] = Client.EVMTokenAmount({token: srcToken, amount: AMOUNT});
// Send via CCIP using native gas as fee
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));
}
}
了解 TransferTokens 腳本
在此腳本中,我們首先檢查終端機中傳入的參數是否為 to-bsc 或 to-hyperevm,以標示轉帳的方向。根據此資訊,我們會為相應的源鏈(HyperEVM 或 BSC)建立一個分叉。我們使用 常數 合約。這些常數包括 路由器 傳送跨鏈訊息所需的地址和鏈選取器。
我們設定要轉移的 qWHYPE 數量為一個固定值,本例中為 0.01 qWHYPE。我們授權 Router 動用我們的 qWHYPE 代幣,並建立一個 Client.EVM2AnyMessage 一個結構體,其中包含我們的跨鏈轉帳詳細資訊,包括收款人地址、代幣數量以及任何額外參數。
最後,我們呼叫該 ccipSend 在 Router 合約中呼叫函式以傳送跨鏈訊息,並以來源鏈的原生代幣(HYPE 或 BNB)支付所需手續費。該腳本會記錄預估手續費以及 CCIP 轉帳的訊息 ID。此訊息 ID 可用於在 Chainlink CCIP 探索器.
太棒了!我們現在已經寫好了所有必要的腳本,用以部署和配置 CCIP 代幣池,並在 HyperEVM 與 BSC 之間執行 qWHYPE 代幣的跨鏈轉移。接下來,我們將在下一節中執行這些腳本。
執行腳本
既然我們已經寫好了所有必要的腳本,現在就可以依序執行它們,以部署和配置我們的 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
我們建議不要直接在命令列中傳入您的私密金鑰,因為它可能會被儲存於您的殼層歷史紀錄中。請務必使用環境變數或安全的方法來處理敏感資訊!
如果您的錢包尚未在 HyperCore 上註冊,此指令可能會因以下原因而失敗: 該使用者或 API 錢包不存在。 請確保您已在 Hyperliquid 上進行過存款或交易,以註冊您的錢包。如果您已按照 從 HyperCore 銜接至 HyperEVM 沒錯,您的錢包已經註冊完成了!
太好了!我們的帳戶現在可以使用更大的區塊了。接下來,我們將使用 DeployTokens.s.sol 腳本:
forge script script/DeployTokens.s.sol:DeployTokens --broadcast --verify --verifier etherscan
此指令將把我們的代幣部署至 HyperEVM 和 BSC 兩個網路。您將在終端機中看到部署進度,待交易確認後,也會顯示交易哈希值。該 --廣播 此標誌表示我們希望將交易傳送至網路,而 --驗證 以及 --驗證器 Etherscan 部署完成後,flags 會自動在 hyperevmscan.io 和 bscscan.com 上驗證我們的合約。您還會發現您的 deployments.json 檔案中已填入已部署的合約地址。您的終端機輸出應類似如下:

接下來,我們將使用 DeployPools.s.sol 腳本:
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 腳本:
forge script script/DepositAndTransferTokens.s.sol:DepositAndTransferTokens --broadcast
此指令將存入 HYPE 以在 HyperEVM 上鑄造 qWHYPE,隨後將 qWHYPE 代幣轉移至 BSC。交易確認後,您將在終端機中看到轉帳進度及交易哈希值。您還會看到該轉帳的 CCIP 訊息 ID,可藉此在Chainlink CCIP Explorer 上追蹤轉帳狀態。您的終端機將輸出如下所示的訊息 ID:
- 終端輸出
- CCIP 探索者
- HyperEVM 瀏覽器
- BSC Explorer
顯示 CCIP 訊息 ID 的終端機輸出
CCIP Explorer 顯示跨鏈轉帳的狀態
HyperEVM Explorer 顯示轉帳交易的詳細資訊
BSC Explorer 顯示已接收轉帳交易的詳細資訊
讓我們來驗證一下這個 qWHYPE 代幣已發送至 BSC 網路。請開啟您的 MetaMask 錢包,切換至 BSC 網路,並匯入該 qWHYPE 使用您在 deployments.json 包含該金鑰的檔案 qWHYPE_bsc. 若您需要有關匯入代幣的協助,請參閱 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 瀏覽器後,現在將顯示反向的轉帳紀錄:

結論
恭喜!您已成功學會如何使用 Chainlink CCIP 在 Hyperliquid 上進行代幣跨鏈轉移。您已部署了自己的 ERC-20 代幣、設定了 CCIP 代幣池,並在 HyperEVM 與 BSC 之間完成了 qWHYPE 代幣的跨鏈轉移。
下一步
既然您已經擁有了一個可運作的跨鏈橋,現在可以進一步優化並客製化您的設定。以下提供一些建議:
- Hyperliquid Bridge UI:利用 React 或 Vue.js 等框架,為您的橋接器建置一個使用者友善的介面。這樣一來,使用者便能輕鬆與您的橋接器互動,無需使用命令列工具。
- 速率限制: 請在您的代幣池中實施速率限制,以防止濫用並確保公平使用。您可以在
ConfigurePools.s.sol腳本。 - 實作更多鏈:擴展您的橋接方案,以支援 Chainlink CCIP 所支援的其他鏈。您可以依照本指南中所述的相同步驟,在其他鏈上部署並配置代幣池。請在CCIP 目錄中查看通道及受支援的鏈。
- 封裝其他 HIP-1 資產:您可以依照與處理 HYPE 時類似的流程,在 Hyperliquid 上為其他 HIP-1 資產建立並部署額外的封裝代幣。唯一的差異在於,每項資產都需要建立一個對應的 ERC-20 封裝合約。此舉將使使用者能夠在 HyperEVM 與其他區塊鏈之間橋接更廣泛的資產。
更多資源
- Hyperliquid 文件
- Quicknode Hyperliquid 使用指南
- Quicknode Hyperliquid 文件
- 《鑄造之書》
- 影片:什麼是 Hyperliquid HyperEVM?以及如何開始使用
如果您遇到困難或有任何疑問,歡迎在我們的Discord 頻道中提出。請追蹤我們的X帳號 (@Quicknode) 或Telegram 公告頻道,以掌握最新動態。
我們 ❤️ 您的回饋!
如果您對這份文件有任何意見或疑問,請告訴我們。我們非常樂意聽取您的意見!