鏈條 CCIP

導言
Chainlink Cross-Chain Interoperability Protocol (CCIP) 為開發人員和分散式應用程式 (dApp) 提供安全、有效的跨區塊鏈互動方式。 透過 CCIP,您可以傳送代幣和任意訊息,以觸發目的合約上的動作,例如鑄造 NFT、重新平衡指數或呼叫自訂函式。
在本教程中,您將學習如何使用 Chainlink CCIP 從 Kaia 智慧型契約傳送訊息和代幣到另一條鏈上的契約,以及如何將訊息和代幣接收回來。
先決條件。
- 代工安裝
- 使用 
curl -L https://foundry.paradigm.xyz | bash安裝,然後執行foundryup。 - 使用 
forge --version,``cast --version和anvil --version進行驗證。 
 - 使用 
 - MetaMask 錢包
- 設定開發人員錢包
 - 將 Kaia Kairos 測試網路和 Ethereum Sepolia 網路加入 MetaMask。
 
 - 從水龍頭測試代幣
- KAIA:從 Kaia 部署和傳送的瓦斯。
 - LINK (testnet):用 LINK 付款時,適用 CCIP 費用。
 - 目的鏈上的原生代幣(例如,Sepolia ETH:用於部署,如果選擇,用於以原生代幣支付 CCIP 費用)。
 
 
開始使用
在本指南中,您將使用 Chainlink CCIP 在 Kaia (Kairos Testnet) 和 Ethereum Sepolia 之間傳送和接收跨鏈訊息。
到最後,您將會
- 初始化為 Kairos 和 Sepolia 設定的 Foundry 專案
 - 新增 Chainlink CCIP 契約和介面為依賴項目
 - 實作一個 Messenger 契約,可跨鏈傳送和接收訊息
 - 部署至兩個網路並驗證往返訊息
 
建立專案
在本節中,您將使用 Foundry 設定開發環境。 若要建立新的 Foundry 專案,請先建立一個新目錄:
mkdir kaia-foundry-ccip-example
那就跑吧
cd kaia-foundry-ccip-exampleforge init
這將以下列基本配置建立一個 Foundry 專案:
├── foundry.toml├── script├── src└── test
安裝 Chainlink 智慧型契約
若要在您的 Foundry 專案中使用 Chainlink CCIP,您需要使用 forge install 安裝 Chainlink CCIP 智慧型契約作為專案的相依性。
若要安裝 Chainlink CCIP 智慧型契約,請執行:
forge install smartcontractkit/chainlink-ccip@2114b90f39c82c052e05af7c33d42c1ae98f4180forge install smartcontractkit/chainlink-evm@ff814eb0a01f89d9a215f825d243bf421e6434a9
安裝完成後,建立一個 remapping.txt 檔案:
forge remappings > remappings.txt
然後將以下內容貼到您新建立的檔案中:
@chainlink/contracts/=lib/chainlink-evm/contracts/@chainlink/contracts-ccip/=lib/chainlink-ccip/chains/evm/contracts/
撰寫智慧型契約
在本節中,您將使用下面的程式碼來跨鏈傳送和接收訊息。
在專案的 rc 目錄下製作一個新檔案,命名為 Messenger.sol,並將下列程式碼複製到檔案中:
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import { IRouterClient } from "@chainlink/contracts-ccip/interfaces/IRouterClient.sol";import {OwnerIsCreator} from "@chainlink/contracts/src/v0.8/shared/access/OwnerIsCreator.sol";import { Client } from "@chainlink/contracts-ccip/libraries/Client.sol";import { CCIPReceiver } from "@chainlink/contracts-ccip/applications/CCIPReceiver.sol";import {IERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/IERC20.sol";import {SafeERC20} from "@chainlink/contracts/src/v0.8/vendor/openzeppelin-solidity/v4.8.3/contracts/token/ERC20/utils/SafeERC20.sol";/** * THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY. * THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE. * DO NOT USE THIS CODE IN PRODUCTION. *//// @title - A simple messenger contract for sending/receiving string data across chains.contract Messenger is CCIPReceiver, OwnerIsCreator {    using SafeERC20 for IERC20;    // Custom errors to provide more descriptive revert messages.    error NotEnoughBalance(uint256 currentBalance, uint256 calculatedFees); // Used to make sure contract has enough balance.    error NothingToWithdraw(); // Used when trying to withdraw Ether but there's nothing to withdraw.    error FailedToWithdrawEth(address owner, address target, uint256 value); // Used when the withdrawal of Ether fails.    error DestinationChainNotAllowlisted(uint64 destinationChainSelector); // Used when the destination chain has not been allowlisted by the contract owner.    error SourceChainNotAllowlisted(uint64 sourceChainSelector); // Used when the source chain has not been allowlisted by the contract owner.    error SenderNotAllowlisted(address sender); // Used when the sender has not been allowlisted by the contract owner.    error InvalidReceiverAddress(); // Used when the receiver address is 0.    // Event emitted when a message is sent to another chain.    event MessageSent(        bytes32 indexed messageId, // The unique ID of the CCIP message.        uint64 indexed destinationChainSelector, // The chain selector of the destination chain.        address receiver, // The address of the receiver on the destination chain.        string text, // The text being sent.        address feeToken, // the token address used to pay CCIP fees.        uint256 fees // The fees paid for sending the CCIP message.    );    // Event emitted when a message is received from another chain.    event MessageReceived(        bytes32 indexed messageId, // The unique ID of the CCIP message.        uint64 indexed sourceChainSelector, // The chain selector of the source chain.        address sender, // The address of the sender from the source chain.        string text // The text that was received.    );    bytes32 private s_lastReceivedMessageId; // Store the last received messageId.    string private s_lastReceivedText; // Store the last received text.    // Mapping to keep track of allowlisted destination chains.    mapping(uint64 => bool) public allowlistedDestinationChains;    // Mapping to keep track of allowlisted source chains.    mapping(uint64 => bool) public allowlistedSourceChains;    // Mapping to keep track of allowlisted senders.    mapping(address => bool) public allowlistedSenders;    IERC20 private s_linkToken;    /// @notice Constructor initializes the contract with the router address.    /// @param _router The address of the router contract.    /// @param _link The address of the link contract.    constructor(address _router, address _link) CCIPReceiver(_router) {        s_linkToken = IERC20(_link);    }    /// @dev Modifier that checks if the chain with the given destinationChainSelector is allowlisted.    /// @param _destinationChainSelector The selector of the destination chain.    modifier onlyAllowlistedDestinationChain(uint64 _destinationChainSelector) {        if (!allowlistedDestinationChains[_destinationChainSelector])            revert DestinationChainNotAllowlisted(_destinationChainSelector);        _;    }    /// @dev Modifier that checks if the chain with the given sourceChainSelector is allowlisted and if the sender is allowlisted.    /// @param _sourceChainSelector The selector of the destination chain.    /// @param _sender The address of the sender.    modifier onlyAllowlisted(uint64 _sourceChainSelector, address _sender) {        if (!allowlistedSourceChains[_sourceChainSelector])            revert SourceChainNotAllowlisted(_sourceChainSelector);        if (!allowlistedSenders[_sender]) revert SenderNotAllowlisted(_sender);        _;    }    /// @dev Modifier that checks the receiver address is not 0.    /// @param _receiver The receiver address.    modifier validateReceiver(address _receiver) {        if (_receiver == address(0)) revert InvalidReceiverAddress();        _;    }    /// @dev Updates the allowlist status of a destination chain for transactions.    function allowlistDestinationChain(        uint64 _destinationChainSelector,        bool allowed    ) external onlyOwner {        allowlistedDestinationChains[_destinationChainSelector] = allowed;    }    /// @dev Updates the allowlist status of a source chain for transactions.    function allowlistSourceChain(        uint64 _sourceChainSelector,        bool allowed    ) external onlyOwner {        allowlistedSourceChains[_sourceChainSelector] = allowed;    }    /// @dev Updates the allowlist status of a sender for transactions.    function allowlistSender(address _sender, bool allowed) external onlyOwner {        allowlistedSenders[_sender] = allowed;    }    /// @notice Sends data to receiver on the destination chain.    /// @notice Pay for fees in LINK.    /// @dev Assumes your contract has sufficient LINK.    /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.    /// @param _receiver The address of the recipient on the destination blockchain.    /// @param _text The text to be sent.    /// @return messageId The ID of the CCIP message that was sent.    function sendMessagePayLINK(        uint64 _destinationChainSelector,        address _receiver,        string calldata _text    )        external        onlyOwner        onlyAllowlistedDestinationChain(_destinationChainSelector)        validateReceiver(_receiver)        returns (bytes32 messageId)    {        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message        Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(            _receiver,            _text,            address(s_linkToken)        );        // Initialize a router client instance to interact with cross-chain router        IRouterClient router = IRouterClient(this.getRouter());        // Get the fee required to send the CCIP message        uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);        if (fees > s_linkToken.balanceOf(address(this)))            revert NotEnoughBalance(s_linkToken.balanceOf(address(this)), fees);        // Approve the Router to transfer LINK tokens on contract's behalf. It will spend the fees in LINK        s_linkToken.approve(address(router), fees);        // Send the CCIP message through the router and store the returned CCIP message ID        messageId = router.ccipSend(_destinationChainSelector, evm2AnyMessage);        // Emit an event with message details        emit MessageSent(            messageId,            _destinationChainSelector,            _receiver,            _text,            address(s_linkToken),            fees        );        // Return the CCIP message ID        return messageId;    }    /// @notice Sends data to receiver on the destination chain.    /// @notice Pay for fees in native gas.    /// @dev Assumes your contract has sufficient native gas tokens.    /// @param _destinationChainSelector The identifier (aka selector) for the destination blockchain.    /// @param _receiver The address of the recipient on the destination blockchain.    /// @param _text The text to be sent.    /// @return messageId The ID of the CCIP message that was sent.    function sendMessagePayNative(        uint64 _destinationChainSelector,        address _receiver,        string calldata _text    )        external        onlyOwner        onlyAllowlistedDestinationChain(_destinationChainSelector)        validateReceiver(_receiver)        returns (bytes32 messageId)    {        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message        Client.EVM2AnyMessage memory evm2AnyMessage = _buildCCIPMessage(            _receiver,            _text,            address(0)        );        // Initialize a router client instance to interact with cross-chain router        IRouterClient router = IRouterClient(this.getRouter());        // Get the fee required to send the CCIP message        uint256 fees = router.getFee(_destinationChainSelector, evm2AnyMessage);        if (fees > address(this).balance)            revert NotEnoughBalance(address(this).balance, fees);        // Send the CCIP message through the router and store the returned CCIP message ID        messageId = router.ccipSend{value: fees}(            _destinationChainSelector,            evm2AnyMessage        );        // Emit an event with message details        emit MessageSent(            messageId,            _destinationChainSelector,            _receiver,            _text,            address(0),            fees        );        // Return the CCIP message ID        return messageId;    }    /// handle a received message    function _ccipReceive(        Client.Any2EVMMessage memory any2EvmMessage    )        internal        override        onlyAllowlisted(            any2EvmMessage.sourceChainSelector,            abi.decode(any2EvmMessage.sender, (address))        ) // Make sure source chain and sender are allowlisted    {        s_lastReceivedMessageId = any2EvmMessage.messageId; // fetch the messageId        s_lastReceivedText = abi.decode(any2EvmMessage.data, (string)); // abi-decoding of the sent text        emit MessageReceived(            any2EvmMessage.messageId,            any2EvmMessage.sourceChainSelector, // fetch the source chain identifier (aka selector)            abi.decode(any2EvmMessage.sender, (address)), // abi-decoding of the sender address,            abi.decode(any2EvmMessage.data, (string))        );    }    /// @notice Construct a CCIP message.    /// @dev This function will create an EVM2AnyMessage struct with all the necessary information for sending a text.    /// @param _receiver The address of the receiver.    /// @param _text The string data to be sent.    /// @param _feeTokenAddress The address of the token used for fees. Set address(0) for native gas.    /// @return Client.EVM2AnyMessage Returns an EVM2AnyMessage struct which contains information for sending a CCIP message.    function _buildCCIPMessage(        address _receiver,        string calldata _text,        address _feeTokenAddress    ) private pure returns (Client.EVM2AnyMessage memory) {        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message        return            Client.EVM2AnyMessage({                receiver: abi.encode(_receiver), // ABI-encoded receiver address                data: abi.encode(_text), // ABI-encoded string                tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array as no tokens are transferred                extraArgs: Client._argsToBytes(                    // Additional arguments, setting gas limit and allowing out-of-order execution.                    // Best Practice: For simplicity, the values are hardcoded. It is advisable to use a more dynamic approach                    // where you set the extra arguments off-chain. This allows adaptation depending on the lanes, messages,                    // and ensures compatibility with future CCIP upgrades. Read more about it here: https://docs.chain.link/ccip/concepts/best-practices/evm#using-extraargs                    Client.GenericExtraArgsV2({                        gasLimit: 200_000, // Gas limit for the callback on the destination chain                        allowOutOfOrderExecution: true // Allows the message to be executed out of order relative to other messages from the same sender                    })                ),                // Set the feeToken to a feeTokenAddress, indicating specific asset will be used for fees                feeToken: _feeTokenAddress            });    }    /// @notice Fetches the details of the last received message.    /// @return messageId The ID of the last received message.    /// @return text The last received text.    function getLastReceivedMessageDetails()        external        view        returns (bytes32 messageId, string memory text)    {        return (s_lastReceivedMessageId, s_lastReceivedText);    }    /// @notice Fallback function to allow the contract to receive Ether.    /// @dev This function has no function body, making it a default function for receiving Ether.    /// It is automatically called when Ether is sent to the contract without any data.    receive() external payable {}    /// @notice Allows the contract owner to withdraw the entire balance of Ether from the contract.    /// @dev This function reverts if there are no funds to withdraw or if the transfer fails.    /// It should only be callable by the owner of the contract.    /// @param _beneficiary The address to which the Ether should be sent.    function withdraw(address _beneficiary) public onlyOwner {        // Retrieve the balance of this contract        uint256 amount = address(this).balance;        // Revert if there is nothing to withdraw        if (amount == 0) revert NothingToWithdraw();        // Attempt to send the funds, capturing the success status and discarding any return data        (bool sent, ) = _beneficiary.call{value: amount}("");        // Revert if the send failed, with information about the attempted transfer        if (!sent) revert FailedToWithdrawEth(msg.sender, _beneficiary, amount);    }    /// @notice Allows the owner of the contract to withdraw all tokens of a specific ERC20 token.    /// @dev This function reverts with a 'NothingToWithdraw' error if there are no tokens to withdraw.    /// @param _beneficiary The address to which the tokens will be sent.    /// @param _token The contract address of the ERC20 token to be withdrawn.    function withdrawToken(        address _beneficiary,        address _token    ) public onlyOwner {        // Retrieve the balance of this contract        uint256 amount = IERC20(_token).balanceOf(address(this));        // Revert if there is nothing to withdraw        if (amount == 0) revert NothingToWithdraw();        IERC20(_token).safeTransfer(_beneficiary, amount);    }}
上面的程式碼是一個雙向的 CCIP 契約,可以在允許清單的鏈上傳送和接收字串訊息,並有擁有者控制、LINK 或原生費用支付。 讓我們來看看本合約中要用到的主要功能:
1. 所有清單
- allowlistSourceChain(selector, allowed):控制允許哪些來源鏈傳送訊息到此契約。
 - allowlistDestinationChain(selector, allowed):控制允許此契約傳送至哪些目的地鏈。
 - allowlistedSenders[address] (via allowlistSender(addr, allowed)):當訊息到達時,限制信任來源鏈上的哪些寄件者位址。
 
測試前在兩端都設定好。 來源必須信任寄件者和連鎖。 目的地也必須允許傳送。
2. 傳送訊息
sendMessagePayLINK(selector, receiver, text):傳送訊息並在 LINK 中支付 CCIP 費用。 這會建立訊息、報價費用、檢查 LINK 結餘、核准 Router,然後執行 ccipSend。 當完成時,它會回傳一個與傳送訊息相關的唯一 ID。
sendMessagePayNative(selector, receiver, text):傳送訊息並以原生代幣支付 CCIP 費用。 這會建立訊息、報價費用、檢查本機餘額,然後執行 ccipSend(value:費用)。 完成後,它會回傳一個與傳送訊息相關的唯一 ID。
3. 建立訊息
_buildCCIPMessage(receiver, text, feeTokenAddress) -> EVM2AnyMessage
- 編碼接收器和文字
 - 未傳送任何代幣(tokenAmounts 為空)
 - 使用 GenericExtraArgsV2 與可設定 gasLimit 的 extraArgs 套件
 - 設定 feeToken 為 LINK 或 address(0)。
 
4. 接收訊息
CCIP 呼叫 _ccipReceive(...) 在目的地鏈上。 合約:
- 根據允許清單驗證來源鏈和寄件者
 - 解碼字串
 - 將其儲存為最後收到的有效負載
 - 輸出 MessageReceived
 - 讀取最後一次入站的有效負載: getLastReceivedMessageDetails() -> (messageId, text)
 
編譯智慧型契約
要編譯您的智慧型契約,請執行
forge build