通过 CCIP 构建跨链应用(5 个案例)

  • Chainlink
  • 更新于 2023-08-02 17:01
  • 阅读 2441

目前,部署在多个区块链上的应用程序面临着资产、流动性和用户的碎片化问题。通过Chainlink CCIP,开发人员可以完成通证和任意信息的跨链传递,实现跨链的去中心化应用程序的创建,这些应用程序由部署在多个不同区块链网络上的智能合约组成,彼此互操作,形成一个统一的应用。

Chainlink 的跨链互操作性协议(CCIP)是一种新的通用跨链通信协议,为智能合约开发人员提供了以最小化信任的方式在区块链网络之间传输数据和通证的能力。

目前,部署在多个区块链上的应用程序面临着资产、流动性和用户的碎片化问题。通过 CCIP,开发人员可以完成通证和任意信息的跨链传递,实现跨链的去中心化应用程序的创建,这些应用程序由部署在多个不同区块链网络上的智能合约组成,彼此互操作,形成一个统一的应用。这种 Web3 设计模式被称为跨链智能合约

以下是 CCIP 在跨链应用中的几个用例示例,涵盖了 DeFi、跨链 ENS、在多个区块链上铸造 NFT 以及跨链游戏。这些用例示例展示了 CCIP 将传统的单链或多链应用转变为功能强大的新型跨链去中心化应用(dApps)的潜力。

所有示例都可以在 Chainlink Labs 的 GitHub 上找到,并且现在可以自行部署并且与之交互。

DeFi:跨链可组合性

DeFi 是 Web3 应用的一个类别,非常适合通过跨链智能合约进行创新。在目前的 DeFi 世界中,不论 dApp 是部署在单个区块链上,还是多个区块链上,每个实例都有自己的用户和流动性。在每个链上,都有 DeFi 的可组合性和“货币乐高”概念,开发人员可以无需许可地连接和集成部署在特定网络上的各种协议,以创建新的用例和金融产品。

通过实现跨链智能合约和跨链通证转移,CCIP 将 DeFi 的可组合性概念进行了指数级的扩展。现在,可组合性不再局限于单个区块链,也不再局限于该链上的 DeFi 协议,而是所有区块链上的所有 DeFi 应用都可以以各种方式组合在一起,从而创造出新的金融产品。基于此设想,应用程序和协议不再受限于所在的区块链。

这种跨链组合性使得 DeFi 应用的生态系统更加互联互通,减少了碎片化,所有链上的流动性、用户和金融产品对所有协议都是可用的。以“货币乐高”的比喻,CCIP 让你将所有不同的乐高积木套装汇聚在一起,将它们视为一个统一的整体来构建金融协议。

其中一个将极大受益于 CCIP 的特定 DeFi 金融服务是借贷。在当今世界,大多数 DeFi 借贷协议要求你在协议所在的链上存入抵押品。但许多 DeFi 用户在多个区块链上使用多个 DeFi 协议,并且他们的资产分散在这些链上。这些用户通常追求最佳回报,调整持仓以最大化收益,但在许多情况下,当资产在一条链上被锁定时,可能在另一条链上存在更好的收益机会。如果他们想获得这些更好的收益,他们需要在一条链上清算其持仓,手动将资产桥接到新的链上,在新链上的协议上存入资产,然后在想要将资产返回原始链时执行相同的过程——这么多的步骤都是为了将资产移入新的协议中追求收益机会。

在这种情况下,CCIP 可以帮助实现 DeFi 协议真正实现跨链,并允许用户在一条链上无缝地将数字资产用作另一条链上 DeFi 协议的抵押品,所有这些都由 CCIP 在协议层面处理,无需用户执行手动步骤,也无需依赖第三方桥接引入信任假设。利用 CCIP,DeFi 协议可以允许借款人在一条(源)链上存入资产,或者直接转移到目标链,然后在目标链上提供这些资产以供借贷。然后,当他们希望停止在目标链上使用其资产时,DeFi 协议可以使用 CCIP 撤回他们的持仓并将其资产转回到原始链。这就是 CCIP 所赋予的 DeFi 协议的能力。

在这个例子中,我们有一个部署在 Avalanche Fuji 测试网上的 DeFi 智能合约 Sender.sol。该合约接受以通证形式的用户质押;它可以是包装过的 ETH、stable coin 或任何具有实际价值的通证。Sender.sol 有一个sendMessage 函数,它使用 CCIP 执行可编程通证转移,以及将消息发送到目标链。在这种情况下,我们将指定的通证发送到 Ethereum Sepolia 测试网,并在消息中包含最终用户的 EOA(外部拥有账户):

// Sender.sol
 Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
receiver: abi.encode(receiver), // ABI-encoded receiver contract address
data: data,
tokenAmounts: tokenAmounts,
extraArgs: Client._argsToBytes(
Client.EVMExtraArgsV1({gasLimit: 200_000, strict: false}) // Additional arguments, setting gas limit and non-strict sequency mode
),
feeToken: address(linkToken) // Setting feeToken to LinkToken address, indicating LINK will be used for fees
});
// Initialize a router client instance to interact with cross-chain router
IRouterClient router = IRouterClient(this.getRouter());
// Get the fee required to send the message. Fee paid in LINK.
uint256 fees = router.getFee(destinationChainSelector, evm2AnyMessage);
// Approve the Router to pay fees in LINK tokens on contract's behalf.
linkToken.approve(address(router), fees);
// Approve the Router to transfer the tokens on contract's behalf.
IERC20(tokenToTransfer).approve(address(router), transferAmount);
// Send the message through the router and store the returned message ID
messageId = router.ccipSend(destinationChainSelector, evm2AnyMessage);
// Emit an event with message details
emit MessageSent(messageId, destinationChainSelector, receiver, msg.sender, tokenAmount, fees);

注意:本文中的所有代码示例仅供说明,并按“原样”提供,没有任何明示或暗示的保证、担保、约定或条件。使用这些代码片段受我们在 chain.link/terms 中提供的服务条款的约束。

在 Ethereum Sepolia 网络上,我们部署了一个 Protocol.sol 智能合约。该合约接收 CCIP 可编程通证转移的消息,并执行以下操作:

  • 铸造和管理可以抵押物品借贷的 stable coin。
  • 从 CCIP 消息中读取指定的通证合约地址(在目标链上),该地址用于标识从源链发送的资金(作为抵押品进行借贷)以及存入的金额。
  • 同样从 CCIP 消息内容中读取最终用户(存款人/借款人)的钱包地址。stable coin 被铸造到该地址,并且该地址还用于跟踪存款和借贷。
  • 将这些信息存储在智能合约中。
// Protocol.sol
   bytes32 messageId = any2EvmMessage.messageId; // fetch the messageId
   uint64 sourceChainSelector = any2EvmMessage.sourceChainSelector; // fetch the source chain identifier (aka selector)
   address sender = abi.decode(any2EvmMessage.sender, (address)); // abi-decoding of the sender address
   // Collect tokens transferred. This increases this contract's balance for that Token.
   Client.EVMTokenAmount[] memory tokenAmounts = any2EvmMessage.destTokenAmounts;
   address token = tokenAmounts[0].token;
   uint256 amount = tokenAmounts[0].amount;
   address depositor = abi.decode(any2EvmMessage.data, (address)); // abi-decoding of the depositor's address
   receivedMessages.push(messageId);
   MessageIn memory detail = MessageIn(sourceChainSelector, sender, depositor, token, amount);
   messageDetail[messageId] = detail;
   emit MessageReceived(messageId, sourceChainSelector, sender, depositor, tokenAmounts[0]);
   // Store depositor data.
   deposits[depositor][token] = amount;

一旦 Protocol.sol 成功接收并处理了 CCIP 可编程通证转移消息,用户就可以手动执行借款操作,通过执行 borrowUSDC 函数来实现。该函数允许用户将转移的通证用作抵押,以铸造并借入等值的 stable coin,例如 USDC,并发送给借款人的 EOA(外部拥有账户)。在这个例子中,我们假设抵押率为 70%,这意味着协议将借出不超过抵押通证价值的 70%:

uint256 borrowed = borrowings[msg.sender][address(usdcToken)];
require(borrowed == 0, "Caller has already borrowed USDC");
address transferredToken = messageDetail[msgId].token;
require(transferredToken != address(0), "Caller has not transferred this token");
uint256 deposited = deposits[msg.sender][transferredToken];
uint256 borrowable = (deposited * 70) / 100; // 70% collateralization ratio.
// LINK/USD on Sepolia (https://sepolia.etherscan.io/address/0xc59E3633BAAC79493d908e63626716e204A45EdF)
// Docs: https://docs.chain.link/data-feeds/price-feeds/addresses#Sepolia%20Testnet
AggregatorV3Interface priceFeed = AggregatorV3Interface(0xc59E3633BAAC79493d908e63626716e204A45EdF);
(, int256 price, , , ) = priceFeed.latestRoundData(); 
uint256 price18decimals = uint256(price * (10 ** 10)); // make USD price 18 decimal places from 8
uint256 borrowableInUSDC = borrowable * price18decimals;
// MintUSDC
usdcToken.mint(msg.sender, borrowableInUSDC);
// Update state.
borrowings[msg.sender][address(usdcToken)] = borrowableInUSDC;
return borrowableInUSDC;

用户成功在 Sepolia 上对其存入的抵押物品借入 USDC 后,他们可以根据自己的意愿在 Sepolia 网络上的任何 DeFi 协议中使用这些资金。然后,当他们完成后,他们可以将资金偿还给 Protocol.sol,这将导致 stable coin 被销毁,并且 CCIP 可编程通证转移消息被发送回 Fuji 网络上的 Sender.sol 合约,这将把被锁定的通证返回到 Fuji 网络上指定的地址。需要注意的是,用户必须首先将 Protocol.sol 设为用户所借入stable coin 的“spender”,以便协议能够销毁所借的金额,这是偿还的实现方式:

require(amount >= borrowings[msg.sender][address(usdcToken)], "Repayment amount is less than amount borrowed");
// Get the deposit details, so it can be transferred back.
address transferredToken = messageDetail[msgId].token;
uint256 deposited = deposits[msg.sender][transferredToken];
uint256 mockUSDCBal = usdcToken.balanceOf(msg.sender);
require(mockUSDCBal >= amount, "Caller's USDC token balance insufficient for repayment");
if (usdcToken.allowance(msg.sender, address(this)) < borrowings[msg.sender][address(usdcToken)]) {
revert("Protocol allowance is less than amount borrowed");
}
usdcToken.burnFrom(msg.sender, mockUSDCBal);
borrowings[msg.sender][address(usdcToken)] = 0;
// send transferred token and message back to Sepolia Sender contract
sendMessage(destinationChain, receiver, transferredToken, deposited);

可以在 CCIP-DeFi Lending 的 GitHub Repo 中找到这个例子的完整源代码和说明。

DeFi:跨链流动性保护

我们继续关于 DeFi 和借贷协议的主题,许多 DeFi 用户在多个区块链上的多个 DeFi 协议中拥有多个持仓。这使得很难跟踪用户的投资组合和 DeFi 持仓。有多个第三方平台、追踪器和收益聚合器可供选择,DeFi 用户可以简单地部署抵押品,并让这些第三方平台处理资产的部署和转移,以优化用户的收益。虽然这些工具是将 DeFi 的一些复杂性抽象出来的很好的方法,让用户可以简单地赚取收益,但它们并不是最小化信任的。用户委托协议来赚取收益,并确保持仓是足额抵押的,以避免清算。除此之外,如果最终用户想要某种清算保护,他们需要在所有拥有 DeFi 持仓的区块链上质押资产,以确保在每个相应链上的持仓贷款有足额的抵押。

通过 CCIP 和跨链通证转移和消息传递,DeFi 协议、持仓监控应用程序和收益聚合器可以增强跨链清算保护。这意味着用户可以在多个区块链上的多个 DeFi 协议中拥有未平仓持仓,然后可以在单个链上分配资产,作为额外的抵押,以确保在某些贷款需要额外资金来保证抵押的情况下使用。以下是其工作原理的简要说明:

  • DeFi最终用户在多个链(例如以太坊、Avalanche、Polygon等)上拥有多个协议的债务持仓,但他们将流动性安全地存放在一个链(例如以太坊上的Aave)的保险库中。

  • 在用户拥有债务持仓的每个链上,用户的Chainlink Automation实现监视持仓的债务比率。

  • 如果 Automation 检测到任何贷款接近清算阈值,然后 Automation 将发送一个 CCIP 消息到用户存放流动性资产的链(例如以太坊),请求资金用于补充债务持仓。

  • 当流动性链上的合约接收到 CCIP 消息时,它会从 Aave 中提取流动性,并将新的 CCIP 消息发送回请求链,包含足够的信息和通证以持仓的质押,并避免清算情况的发生。

由此产生的结果是,用户可以在多个链上拥有债务持仓,同时仍然将流动性保留在单个链上。整个过程是最小化信任的,用户仍然完全控制他们的债务持仓,并且不必手动提取和转移资金到不同的链。以下是其工作原理:

Chainlink Automation 监视用户拥有债务持仓的所有链,并确定是否需要发送资金的消息。如果需要,performUpkeep 函数将发送一个 CCIP 消息到拥有流动性的链上的保险库,请求发送资金。

function checkUpkeep(
       bytes calldata checkData
   )
       external
       view
       override
       returns (bool upkeepNeeded, bytes memory performData)
   {
       upkeepNeeded =
           IMockLending(i_lendingAddress).healthFactor(i_onBehalfOf) <
           i_minHealthFactor &&
           !_isCcipMessageSent;
   }
function performUpkeep(bytes calldata performData) external override {
       require(
           !_isCcipMessageSent,
           "CCIP Message already sent, waiting for a response"
       );
       require(
           IMockLending(i_lendingAddress).healthFactor(i_onBehalfOf) <
               i_minHealthFactor,
           "Account can't be liquidated!"
       );
       // Ask for funds from LPSC on the source blockchain
       Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
           receiver: abi.encode(i_lpsc),
           data: abi.encode(
               i_tokenAddress,
               IMockLending(i_lendingAddress).getBorrowedAmount(i_onBehalfOf)
           ),
           tokenAmounts: new Client.EVMTokenAmount[](0),
           extraArgs: "",
           feeToken: i_link
       });
       bytes32 messageId = IRouterClient(i_sourceChainRouter).ccipSend(
           i_sourceChainSelector,
           message
       );
       _isCcipMessageSent = true;
       emit MessageSent(messageId);
   }

拥有流动性的链上的保险库(vault)接收到资金请求后,会检查是否有足够的资金发送回请求链,或者是否应该从 DeFi 协议(例如Aave)中提取一些流动性,以确保有足够的资金发送。在此之后,它会启动一个 CCIP 可编程通证转移,其中包含请求的资金以及最初接收到的消息 ID(以便在请求资金的目标区块链上的目标合约知道这些资金是用于哪个请求的):

function _ccipReceive(
       Client.Any2EVMMessage memory receivedMessage
   ) internal override {
       bytes32 messageId = receivedMessage.messageId;
       uint64 sourceChainSelector = receivedMessage.sourceChainSelector;
       address sender = abi.decode(receivedMessage.sender, (address));
       (address tokenAddress, uint256 amount) = abi.decode(
           receivedMessage.data,
           (address, uint256)
       );
       address tokenToReturn = s_destinationToSourceMap[
           keccak256(abi.encodePacked(tokenAddress, sourceChainSelector))
       ];
       uint256 currentBalance = IERC20(tokenToReturn).balanceOf(address(this));
       // If there are not enough funds in LPSC, withdraw additional from Aave vault
       if (currentBalance < amount) {
           withdrawFromVault(tokenToReturn, amount - currentBalance);
       }
       Client.EVMTokenAmount[] memory tokenAmounts;
       tokenAmounts[1] = (Client.EVMTokenAmount(tokenToReturn, amount));
       Client.EVM2AnyMessage memory messageReply = Client.EVM2AnyMessage({
           receiver: abi.encode(sender),
           data: abi.encode(msgId),
           tokenAmounts: tokenAmounts,
           extraArgs: "",
           feeToken: LINK
       });
       bytes32 replyMessageId = IRouterClient(i_router).ccipSend(
           sourceChainSelector,
           messageReply
       );
       //  emit ReplySent(replyMessageId,sourceChainSelector, messageId, sender, tokenToReturn, amount);
   }

最后,请求资金以补充债务持仓的链上智能合约接收 CCIP 可编程通证转移,将请求 ID 与其原始请求进行匹配,然后将资金取出,并将转移的资金存入债务持仓中,以增加贷款的抵押率并避免清算:

function _ccipReceive(
        Client.Any2EVMMessage memory receivedMessage
    ) internal override {
        _isCcipMessageSent = false;
        bytes32 requestMessageId = abi.decode(receivedMessage.data, (bytes32));
        uint256 amountToRepay = requested[requestMessageId];
        IMockLending(i_lendingAddress).repay(
            i_tokenAddress,
            amountToRepay,
            i_onBehalfOf
        );
    }

这个例子展示了 CCIP 在 DeFi 协议和 DeFi 持仓监控中的应用场景,为用户在多个区块链上的债务持仓提供了最小化信任的清算保护,同时允许他们将资金和流动性保留在单个区块链上。

可以在CCIP Liquidation Protector的GitHub存储库中找到这个例子的完整源代码和说明。

跨链域名服务

去中心化的域名服务(DNS)如ENS(Ethereum Name Service)在 Web3 中非常受欢迎,因为它们方便了将人类可读的名称转换为钱包地址。在理想的情况下,域名服务不应该局限于一个链,而是每个注册的域名应该在所有以太坊链、侧链、Layer 2 和应用链之间传播和存在。这将允许用户在整个以太坊生态系统中拥有一个单一统一的身份,而不必在多个命名服务上注册域名,也不需要使用非最小化信任的跨链解决方案。

然而,要实现这一点,域名服务需要与其他区块链进行通信。在一个区块链上的每个域名服务实例需要在有新的域名注册时得到通知,并且需要有一种方式在所有区块链上执行“查找”,以对全局名称注册表进行查找。

这个例子展示了如何构建一个简化的跨链命名服务应用,用户可以在一个链上注册域名,并使该注册在多个其他链上传播,同时还可以在任何区块链上解析名称到地址。

cross-chain_0.webp 跨链命名架构概览

第一步是将 CrossChainNameServiceRegisterCrossChainNameServiceLookup 合约部署到Ethereum Sepolia 网络。这个网络将充当“主”网络,所有注册将在这里进行,然后传播到其他链上。

当你注册一个新的 .ccns 接口时,CrossChainNameServiceRegister 合约将使用 CCIP 发送一条消息到其他支持的区块链,其中包含有关已注册的 .ccns 接口的信息:

uint256 length = s_chains.length;
       for (uint256 i; i < length; ) {
           Chain memory currentChain = s_chains[i];
           Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
               receiver: abi.encode(currentChain.ccnsReceiverAddress),
               data: abi.encode(_name, msg.sender),
               tokenAmounts: new Client.EVMTokenAmount[](0),
               extraArgs: Client._argsToBytes(
                   Client.EVMExtraArgsV1({
                       gasLimit: currentChain.gasLimit,
                       strict: currentChain.strict
                   })
               ),
               feeToken: address(0) // We leave the feeToken empty indicating we'll pay raw native.
           });
           i_router.ccipSend{
               value: i_router.getFee(currentChain.chainSelector, message)
           }(currentChain.chainSelector, message);
           unchecked {
               ++i;
           }
       }
       i_lookup.register(_name, msg.sender);

在所有支持的接收链上,将部署 CrossChainNameServiceReceiver 合约。这个合约将接收来自 CrossChainNameServiceRegister 合约的已注册的 .ccns 域,并将它们存储在部署在该区块链上的 CrossChainNameServiceLookup 合约中:

constructor(
        address router,
        address lookup,
        uint64 sourceChainSelector
    ) CCIPReceiver(router) {
        i_router = IRouterClient(router);
        i_lookup = ICrossChainNameServiceLookup(lookup);
        i_sourceChainSelector = sourceChainSelector;
    }
    function _ccipReceive(
        Client.Any2EVMMessage memory message
    ) internal override onlyFromSourceChain(message.sourceChainSelector) {
        (string memory _name, address _address) = abi.decode(
            message.data,
            (string, address)
        );
        i_lookup.register(_name, _address);
    }

最后,CrossChainNameServiceLookup 合约将在所有区块链上部署,包括注册链(在本例中是Sepolia),以及所有目标链。这个合约将用于存储所有已注册的.ccns,并作为一个接口,用于执行查找操作,将名称转换为地址:

function register(
        string memory _name,
        address _address
    ) external onlyCrossChainNameService {
        if (lookup[_name] != address(0)) revert AlreadyTaken();
        lookup[_name] = _address;
    }

使用这个简单的设计模式,可以创建一个简单的跨链域名服务,用户可以注册一个域名,然后在多个区块链上拥有和使用它。

可以在Cross-Chain Name Service的GitHub存储库中找到这个例子的完整源代码和说明。

跨链 NFTs

NFT(非同质化通证)是 Web3 中最受欢迎的用例之一。通常,每个 NFT 项目都部署在单个区块链上,或者该项目本身在多个链上有多个部署,最终用户如果想在多个区块链上拥有 NFT,则需要多次铸造该 NFT。

通过 CCIP Arbitrary Messaging,NFT 项目可以允许其资产在单个链上只铸造一次,由铸造者支付一次费用,然后将它们传播到其他链上的用户。这意味着用户可以拥有和共享他们的 NFT,而不受他们使用哪个网络的影响。CCIP 还可以用于在不同链上“烧毁和铸造”NFT,允许用户将他们的 NFT 从一个链移动到另一个链。以下是第一个场景的示例:

MyNFT 合约包含一个简单的NFT智能合约,其中包含一个铸造函数:

function mint(address to) public {
    unchecked {
        tokenId++;
    }
    _safeMint(to, tokenId);
}

sourceMinter 合约被部署在源链上,它的铸造函数中包含逻辑,用于发送一个 CCIP 跨链消息,其中包含来自MyNFT.sol 智能合约的 ABI 编码的铸造函数签名,发送到目标区块链:

function mint(
        uint64 destinationChainSelector,
        address receiver,
        PayFeesIn payFeesIn
    ) external {
        Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
            receiver: abi.encode(receiver),
            data: abi.encodeWithSignature("mint(address)", msg.sender),
            tokenAmounts: new Client.EVMTokenAmount[](0),
            extraArgs: "",
            feeToken: payFeesIn == PayFeesIn.LINK ? i_link : address(0)
        });
        uint256 fee = IRouterClient(i_router).getFee(
            destinationChainSelector,
            message
        );
        bytes32 messageId;
        if (payFeesIn == PayFeesIn.LINK) {
            LinkTokenInterface(i_link).approve(i_router, fee);
            messageId = IRouterClient(i_router).ccipSend(
                destinationChainSelector,
                message
            );
        } else {
            messageId = IRouterClient(i_router).ccipSend{value: fee}(
                destinationChainSelector,
                message
            );
        }
        emit MessageSent(messageId);

DestinationMinter 智能合约将接收 CCIP 跨链消息,并将 ABI 编码的铸造函数签名作为消息的 payload。然后,它将使用该签名调用 MyNFT 智能合约中的 mint 函数。MyNFT 智能合约将使用SourceMinter 智能合约的 mint() 函数为 msg.sender 账户铸造新的 NFT,即在源链上铸造该 NFT 时的相同账户地址:

function _ccipReceive(
    Client.Any2EVMMessage memory message
) internal override {
    (bool success, ) = address(nft).call(message.data);
    require(success);
    emit MintCallSuccessfull();
}

最终的结果是,铸造了 NFT 的用户现在在多个链上拥有该 NFT,而且他们只需要铸造和支付一次。如果 NFT 项目希望即使在不同区块链上也保持严格的非同质性,这个解决方案也可以很容易地修改为在目标区块链上铸造 NFT,并在之后在源链上销毁,确保在所有区块链上只有一个版本。

可以在Cross-Chain NFT的GitHub存储库中找到这个例子的完整源代码和说明。

游戏:跨链 Tic-Tac-Toe

在过去的几年里,Web3 游戏变得非常受欢迎。然而,就像 DeFi 一样,游戏也非常碎片化,游戏及其资产通常特定于某个区块链。但就像传统游戏一样,最终的目标或最佳体验是当玩家可以一起玩游戏,无论他们使用的硬件或软件是什么。就像 PC 玩家可以与 Xbox 控制台用户一起玩游戏一样,没有理由某人不能在Polygon上玩游戏,而其他人在Avalanche上玩。这就是所谓的跨平台游戏

这个理念完美地适用于基于回合制的 Web3 游戏和其他不需要快速实时互动的游戏。Web3 游戏面临着用户碎片化的问题,玩家更喜欢在他们选择的区块链上玩游戏并使用他们喜欢的数字资产。CCIP 使得 Web3 游戏能够真正实现跨链,促进跨链资产的转移,并在多个区块链上实现共享游戏状态,允许玩家在不考虑使用的链的情况下相互对战。如果你想尽可能地吸引更多的玩家,那么在多个链上部署你的游戏,并设计一个让所有玩家都能相互对战或合作的方式是很有意义的。

通过一个回合制策略游戏(如井字棋)的简单演示,可以展示这个跨链游戏设计模式。在这个例子中,我们有一个游戏智能合约,它被部署在多个区块链上。然后用户可以在他们选择的链上开始游戏,并与朋友分享游戏会话ID。他们的朋友可以从另一个链上加入游戏。在游戏创建时,CCIP 会将游戏细节和初始状态与所有其他链共享:

struct GameSession {
        bytes32 sessionId;
        address player_1; // player who starts the game
        address player_2; // the other player in the game
        address winner; // winner of game
        address turn; // check who takes action in next step
        uint8[9] player1Status; // current status for player 1
        uint8[9] player2Status; // current status for player 2
    }
    mapping(bytes32 => GameSession) public gameSessions;
    bytes32[] public sessionIds;
function start(uint64 destinationChainSelector, address receiver) external {
        bytes32 uniqueId = keccak256(abi.encodePacked(block.timestamp, msg.sender));
        sessionIds.push(uniqueId);
        gameSessions[uniqueId]= GameSession(
            uniqueId,
            msg.sender,
            address(0),
            address(0),
            msg.sender,
            initialCombination,
            initialCombination
            );
        sendMessage(destinationChainSelector, receiver, gameSessions[uniqueId]);
    }

一旦第一位玩家在开始游戏后进行了一次回合,另一个区块链上的第二位玩家在 CCIP 消息成功处理后,将在他们的游戏智能合约中看到更新后的游戏状态。玩家 2 随后将轮到他们进行操作,这将生成一个 CCIP 消息发送回给玩家 1,并在他们的链上更新游戏状态:

function _ccipReceive(
        Client.Any2EVMMessage memory any2EvmMessage
    ) internal override {
        bytes32 messageId = any2EvmMessage.messageId; // fetch the messageId
        uint64 sourceChainSelector = any2EvmMessage.sourceChainSelector; // fetch the source chain identifier (aka selector)
        address sender = abi.decode(any2EvmMessage.sender, (address)); // abi-decoding of the sender address
        GameSession memory message = abi.decode(any2EvmMessage.data, (GameSession)); // abi-decoding of the sent string message
        receivedMessages.push(messageId);
        Message memory detail = Message(sourceChainSelector, sender, message);
        messageDetail[messageId] = detail;
        gameSessions[message.sessionId] = message;
        sessionIds.push(message.sessionId);
        emit MessageReceived(messageId, sourceChainSelector, sender, message);
    }
 function move(
        uint256 x, 
        uint256 y, 
        uint256 player, 
        bytes32 sessionId, 
        uint64 destinationChainSelector,
        address receiver) 
        public  
    {
        GameSession memory gs = gameSessions[sessionId];
        // make sure the game session setup and not over.
        require(gs.player_1 != address(0), "the session is not setup, please start game first!");
        require(gs.winner == address(0), "the game is over");

        // make sure the player is in the game session
        require(player == 1 || player == 2, "you must be player1 or player2"); //this is used to when player has the same address

        if(player == 1) {
            // make sure it is player1's turn to move
            require(gs.player_1 == msg.sender && gs.turn == msg.sender, "it is not your turn");

            // 1. if the position is not taken by the opponent, then take the position
            if(gs.player1Status[x * 3 + y] == 0 && gs.player2Status[x * 3 + y] == 0) {
               gameSessions[sessionId].player1Status[x * 3 + y] = 1;

               // 2. check if player1 wins or make the turn to the opponent, send the message
               if(checkWin(keccak256(abi.encodePacked(gameSessions[sessionId].player1Status)))) {
                   gameSessions[sessionId].winner = gameSessions[sessionId].player_1;
               } else {
                   gameSessions[sessionId].turn = gameSessions[sessionId].player_2;
               }
               sendMessage(destinationChainSelector, receiver, gameSessions[sessionId]);
            } else {
                revert("the position is occupied");
            }
        } else if(player == 2) {
            // make sure it is player2's turn to move, this is the first step for player2
            require((gs.player_2 == msg.sender && gs.turn == msg.sender) || gs.player_2 == address(0), "it is not your turn");
            if(gs.player_2 == address(0)) {
                gameSessions[sessionId].player_2 = msg.sender;
            }
            // 1. if the position is not taken by the opponent, then take the position
            if(gs.player1Status[x * 3 + y] == 0 && gs.player2Status[x * 3 + y] == 0) {
               gameSessions[sessionId].player2Status[x * 3 + y] = 1; 
               // 2. check if player1 wins or make the turn to the opponent, send the message
               if(checkWin(keccak256(abi.encodePacked(gameSessions[sessionId].player2Status)))) {
                   gameSessions[sessionId].winner = gameSessions[sessionId].player_2;
               } else {
                   gameSessions[sessionId].turn = gameSessions[sessionId].player_1;
               }
               sendMessage(destinationChainSelector, receiver, gameSessions[sessionId]);
            } else {
                revert("the position is occupied");
            }
        }
    }

然后,玩家1将再次看到更新后的游戏状态并进行他们的下一步。在游戏参与者进行操作的过程中,CCIP 消息在不同链之间来回传递,直到游戏达到结论并宣布获胜者。需要注意的重要一点是,两个链上的游戏智能合约都保持着游戏状态,CCIP 用于发送和接收消息,以确保游戏状态在两个区块链上保持一致:

function checkWin(bytes32 combination) public view returns (bool) {
        return wcs[combination];
    }
    /// @notice Sends data to receiver on the destination chain.
    /// @dev Assumes your contract has sufficient native asset (e.g, ETH on Ethereum, MATIC on Polygon...).
    /// @param destinationChainSelector The identifier (aka selector) for the destination blockchain.
    /// @param receiver The address of the recipient on the destination blockchain.
    /// @param message The string message to be sent.
    /// @return messageId The ID of the message that was sent.
    function sendMessage(
        uint64 destinationChainSelector,
        address receiver,
        GameSession memory message
    ) public returns (bytes32 messageId) {
        // Create an EVM2AnyMessage struct in memory with necessary information for sending a cross-chain message
        Client.EVM2AnyMessage memory evm2AnyMessage = Client.EVM2AnyMessage({
            receiver: abi.encode(receiver), // ABI-encoded receiver address
            data: abi.encode(msg), // ABI-encoded string message
            tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array indicating no tokens are being sent
            extraArgs: Client._argsToBytes(
                Client.EVMExtraArgsV1({gasLimit: 400_000, strict: false}) // Additional arguments, setting gas limit and non-strict sequency mode
            ),
            feeToken: address(0) // Setting feeToken to zero address, indicating native asset will be used for fees
        });
        // Initialize a router client instance to interact with cross-chain router
        IRouterClient router = IRouterClient(_router);
        // Get the fee required to send the message
        uint256 fees = router.getFee(destinationChainSelector, evm2AnyMessage);
        // Send the message through the router and store the returned message ID
        messageId = router.ccipSend{value: fees}(
            destinationChainSelector,
            evm2AnyMessage
        );
        // Emit an event with message details
        emit MessageSent(
            messageId,
            destinationChainSelector,
            receiver,
            message,
            fees
        );
        // Return the message ID
        return messageId;
    }

cross-chain_1.png 可以在 CCIP Tic-Tac-Toe 的 GitHub 存储库中找到这个例子的完整源代码和说明。

结论

从跨链 DeFi 和 NFT 到在多个区块链上运行的游戏,CCIP 使得跨链智能合约成为可能,实现了在所有区块链上的真正DeFi组合性,并实现了更加统一的Web3生态。

欢迎关注 Chainlink 预言机并且私信加入开发者社区,有大量关于智能合约的学习资料以及关于区块链的话题!

免责声明:本文仅供信息目的,包含有关未来的声明,包括预期的产品功能、开发进度和功能的推出时间。这些陈述仅是预测,反映目前对未来事件的信仰和期望,基于假设,可能面临风险、不确定性和随时变化。Chainlink CCIP目前处于“早期访问”开发阶段,这意味着Chainlink CCIP目前具有正在开发的功能,将在后续版本中进行更改。尽管我们认为这些陈述是基于合理的假设,但不能保证实际结果不会与这些陈述中所表达的有实质性的不同。所有陈述仅在首次发布日期有效。由于用户反馈或后续事件可能导致未来的发展,这些陈述可能不会得到更新,我们可能不会对此进行回应。Chainlink CCIP是一个消息传输协议,不持有或转移任何资产。请查阅Chainlink服务条款,以获取重要信息和披露内容。

点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Chainlink
Chainlink
顶尖的智能合约去中心化预言机网络解决方案 https://chain.link/