Chainlink CCIP – podróżując przez równoległe światy
Liczba dostępnych blockchainów, na których można opierać zdecentralizowane rozwiązania, stale rośnie. Niektóre z nich cechują się większą szybkością, inne niższymi kosztami, a jeszcze inne gwarantują wyższy poziom bezpieczeństwa. W zależności od naszych priorytetów, powinniśmy obrać odpowiednią ścieżkę. Niestety, po związywaniu się z konkretnym blockchainem, rezygnacja z niego może okazać się trudnym zadaniem. Co by się jednak stało, gdyby początkowy wybór nie miał tak dużego znaczenia, a przeniesienie danych z jednego łańcucha na drugi było możliwe w dowolnym momencie? Właśnie w tym kontekście pojawia się rozwiązanie zaproponowane przez Chainlinka – Cross-Chain Interoperability Protocol (CCIP), które umożliwia taką elastyczność.
Prawdziwie bezpieczny i zdecentralizowany most
Przenoszenie danych między różnymi blockchainami to temat nie nowy. Zazwyczaj dotyczyło to przemieszczania różnego rodzaju tokenów pomiędzy różnymi łańcuchami, a mechanizmy umożliwiające to nazywane są mostami. O nich również wspominałem w jednym z wpisów na tym blogu, a szczegółowy opis znajdziesz pod tym linkiem.
W ramach tego wpisu wyjaśniałem nie tylko istotę tych rozwiązań, ale również zwróciłem uwagę na pewne ich wady. Dla przypomnienia, dwie główne to po pierwsze, centralizacja niektórych z tych mechanizmów, co zmusza nas do zaufania ich twórcom w kwestii uczciwości działań. Po drugie, zdecentralizowane opcje często niosą ze sobą luki bezpieczeństwa, które przyczyniają się do dużych strat i kompromitacji.
Chainlink CCIP to nowa propozycja od twórców jednej z najbardziej uznanych instytucji w świecie blockchaina. Produkt ten ma potencjał, by rozwiązać wspomniane problemy i połączyć różne łańcuchy blockchain w sposób zdecentralizowany i bezpieczny. Warto podkreślić, że CCIP nie ogranicza się do roli prostego mostu do przesyłania tokenów. Zgodnie z dokumentacją, stanowi interfejs, który umożliwia łatwą wymianę dowolnych wiadomości pomiędzy różnymi łańcuchami. To otwiera wiele nowych możliwości, takich jak tworzenie:
- międzyplatformowych protokołów DeFi,
- wieloplatformowych NFT,
- wieloplatformowych DAO,
- delegowanie obliczeń na bardziej ekonomiczne blockchainy.
To tylko kilka przykładów, a nasze wyobraźnie stawia tu jedynie ograniczenia. Od teraz łączenie rozwiązań z różnych blockchainów staje się znacznie prostsze. Kluczowe jest to, że cały ten proces opiera się na infrastrukturze sieci Chainlink, która jest już sprawdzonym, dojrzałym i bezpiecznym rozwiązaniem.
Warto jednak zaznaczyć, że ta propozycja jest jeszcze w fazie rozwoju i niektóre funkcje mogą ulec zmianie przed ostatecznym wdrożeniem. Niemniej już teraz możemy korzystać z dobrodziejstw oferowanych przez Chainlink i zacząć eksperymentować na sieciach testowych, albo nawet ubiegać się o pełen dostęp do głównych sieci.
Tutaj klikam, a pojawiam się gdzie indziej
Aby nie ograniczać się jedynie do teorii, przytoczę przykład bardzo prostego scenariusza, który ilustruje, jak możliwe jest przesyłanie danych z jednego blockchaina na drugi. Skupimy się konkretnie na sytuacji, w której chcemy przesłać pewną liczbę z jednego łańcucha do drugiego. Do osiągnięcia tego celu potrzebne będą dwa smart kontrakty. Pierwszy z nich ma następującą postać:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.21;
import {IRouterClient} from "@chainlink/contracts-ccip/src/v0.8/ccip/interfaces/IRouterClient.sol";
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {LinkTokenInterface} from "@chainlink/contracts/src/v0.8/interfaces/LinkTokenInterface.sol";
contract MessageBridge {
IRouterClient private immutable router;
LinkTokenInterface private immutable linkToken;
constructor(address _router, address _link) {
router = IRouterClient(_router);
linkToken = LinkTokenInterface(_link);
}
function sendValue(
uint64 destinationChainSelector,
address destinationContract,
uint256 _value
) external returns (bytes32) {
Client.EVM2AnyMessage memory message = Client.EVM2AnyMessage({
receiver: abi.encode(destinationContract), // ABI-encoded receiver address
data: abi.encode(_value), // ABI-encoded string
tokenAmounts: new Client.EVMTokenAmount[](0), // Empty array indicating no tokens are being sent
extraArgs: Client._argsToBytes(
// Additional arguments, setting gas limit and non-strict sequencing mode
Client.EVMExtraArgsV1({gasLimit: 200_000, strict: false})
),
// Set the feeToken address, indicating LINK will be used for fees
feeToken: address(linkToken)
});
uint256 fees = router.getFee(destinationChainSelector, message);
require(
linkToken.balanceOf(address(this)) >= fees,
"Not enough Link tokens in the contract"
);
linkToken.approve(address(router), fees);
bytes32 messageId = router.ccipSend(destinationChainSelector, message);
return messageId;
}
}
Na pierwszy rzut oka, kod może sprawiać wrażenie skomplikowanego, jednak na szczęście taki nie jest. Przeanalizujmy go teraz, aby się o tym przekonać.
Nasz kontrakt będzie przechowywać dwie wartości. Pierwsza z nich, oznaczona jako “router“, to implementacja kontraktu, która posłuży nam do obliczenia kosztów transakcji oraz wysłania liczby na drugi blockchain. Natomiast pod polem “linkToken” znajdować się będzie kontrakt związanym z tokenem LINK, który pomoże nam pokryć opłaty transakcyjne. Chociaż przy użyciu CCIP możemy pokrywać koszty w innej kryptowalucie niż LINK, w naszym przykładzie posłużymy się właśnie tym tokenem.
Aby przesłać wartość, wykorzystamy funkcję “send“, która wymaga trzech wartości: identyfikatora sieci, na którą ma zostać wysłana wiadomość, adresu docelowego kontraktu oraz wartości do przesłania.
Początek metody polega na zdefiniowaniu wiadomości, która zostanie przekazana do sieci Chainlinka. Wykorzystamy do tego dostarczoną bibliotekę i strukturę “EVM2AnyMessage“. W tym celu musimy zadeklarować kilka wartości. Na początek przekazujemy adres docelowego kontraktu, zakodowany w odpowiedni sposób, a także postępujemy tak samo z naszą wartością liczbową. Ponieważ w tej operacji nie będzie miała miejsca przesyłka tokenów, kolejne pole zostawiamy puste. Następnie konfigurujemy maksymalną ilość gazu dla operacji na docelowym kontrakcie oraz wyłączamy tryb “strict mode“. Bez zagłębiania się w szczegóły, chodzi o to, aby możliwe było przesłanie kolejnej wiadomości nawet wtedy, gdy poprzednia nie została jeszcze przetworzona. Na końcu ustawiamy kryptowalutę, którą wykorzystamy do pokrycia opłat – w naszym przypadku jest to wcześniej wspomniany LINK.
Następnie wyliczamy koszt wysłania naszej wiadomości. Na szczęście nie musimy tego robić ręcznie – korzystamy z gotowej funkcji. Dalej sprawdzamy, czy ilość LINK w kontrakcie wystarcza do pokrycia opłaty, a jeśli tak, wykonujemy operację “approve” na tokenie. Jest to istotne, ponieważ to “router” ostatecznie będzie pobierał opłatę w naszym imieniu.
Pozostało nam teraz tylko wywołać odpowiednią funkcję do przesłania wiadomości, przekazując niezbędne parametry. Gotowe – informacja została przekazana. Na zakończenie zwracamy jeszcze identyfikator naszej wiadomości, który otrzymamy od Chainlinka.
Pierwszy kontrakt jest już omówiony, więc możemy przejść do kolejnego, który będzie przechowywał przesłaną wartość na oddzielnym blockchainie.
//SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {Client} from "@chainlink/contracts-ccip/src/v0.8/ccip/libraries/Client.sol";
import {CCIPReceiver} from "@chainlink/contracts-ccip/src/v0.8/ccip/applications/CCIPReceiver.sol";
contract Storage is CCIPReceiver {
uint256 public value;
constructor(address router) CCIPReceiver(router) {}
function _ccipReceive(
Client.Any2EVMMessage memory any2EvmMessage
) internal override {
value = abi.decode(any2EvmMessage.data, (uint256));
}
}
Tym razem kod jest zdecydowanie bardziej zwięzły. Kluczowe jest, aby nasz kontrakt dziedziczył po “CCIPReceiver“, co umożliwi mu odbieranie przesyłanych wiadomości i zapisywanie ich wartości w polu “value“. W celu osiągnięcia tego celu musimy przesłonić dziedziczoną funkcję “_ccipReceive“, która zostanie wywołana przez sieć Chainlinka.
Ta metoda ma jeden parametr, który reprezentuje otrzymaną wiadomość. W jej wnętrzu dekodujemy przesłaną liczbę i zapisujemy wynik w polu przechowywanej wartości. To w zasadzie wystarcza, jeśli chodzi o kodowanie. Teraz pozostaje nam tylko sprawdzić, czy to rozwiązanie faktycznie będzie działać.
Startujemy
Aby przetestować nasze rozwiązanie, skorzystamy z dwóch testowych sieci: Sepolia, czyli testnet dla Ethereum, oraz Mumbai, odpowiednik Sepolii dla blockchainu Polygon. Pierwszym krokiem jest pozyskanie środków potrzebnych do wykonania transakcji, zarówno w postaci natywnych monet, jak i tokena LINK. Ze względu na wcześniejsze omówienia w różnych wpisach, nie będę ponownie tłumaczyć całego procesu, ale przypomnę tylko źródła, które w tym pomogą. Można zdobyć testowe ethery i LINK na sieci Sepolia za pomocą tego linku, natomiast monety MATIC potrzebne na sieci Mumbai dostępne są pod tym adresem. Po uzyskaniu tokenów, przechodzimy do narzędzia Remix, które wcześniej wielokrotnie używaliśmy do interakcji z kontraktami.
Na początku wgrywamy pierwszy z naszych stworzonych kontraktów na sieć Sepolia. Aby to zrobić, potrzebujemy znać wartości niezbędnych parametrów konstruktora. Możemy je sprawdzić w dokumentacji Chainlinka. W naszym przypadku będą to:
- _router -> 0xD0daae2231E9CB96b94C8512223533293C3693Bf
- _link -> 0x779877A7B0D9E8603169DdbD7836e478b4624789
Następnie zmieniamy sieć w Metamasku na Mumbai i wgrywamy drugi kontrakt. Również w tym przypadku niezbędny parametr znajduje się w dokumentacji:
- _router -> 0x70499c328e1E2a3c41108bd3730F6670a44595D1
Teraz, ponownie w Metamasku, musimy ustawić sieć na Sepolia i przesłać trochę tokenów LINK na adres pierwszego kontraktu. 1 token powinien w zupełności wystarczyć.
Kolejnym krokiem jest wywołanie funkcji “sendValue“. W tym miejscu także skorzystamy z dokumentacji do pozyskania jednej z wartości parametrów:
- destinationChainSelector -> 12532609583862916517
Adres docelowego kontraktu kopiujemy z Remixa, a liczba do przesłania może być dowolna. W moim przypadku jest to 10. Wykonujemy transakcję i pozostaje nam tylko czekać. Czas oczekiwania może być różny – czasami krótki, innym razem nawet kilkanaście minut. Bieżący status naszej transakcji na Chainlinku możemy śledzić pod tym adresem, szukając jej za pomocą np. hasha transakcji z sieci Sepolia lub adresu jednego z naszych inteligentnych kontraktów. Po zakończeniu procesu, przesłana wartość powinna być zapisana na sieci Mumbai.
Podsumowując, we wpisie omówiliśmy istotne zagadnienie przesyłania danych między różnymi blockchainami przy użyciu Chainlink Cross-Chain Interoperability Protocol (CCIP). Przedstawiliśmy, jakie wyzwania mogą pojawić się przy przenoszeniu tokenów oraz jak CCIP ma potencjał, aby rozwiązać te problemy poprzez umożliwienie elastycznego przesyłania dowolnych wiadomości między łańcuchami. Przedstawiony przykład, choć trywialny, to pokazał jak łatwo można skorzystać z tego rozwiązania. Polecam natomiast zapoznać się szczegółowo z dokumentacją i spróbować stworzyć coś bardziej skomplikowanego :).
Warto podkreślić, że Chainlink CCIP otwiera nowe perspektywy dla interakcji między różnymi platformami blockchainowymi. To elastyczne i zdecentralizowane rozwiązanie ma potencjał, aby poprawić interoperacyjność między różnymi blockchainami oraz dostarczyć fundament dla budowy złożonych systemów międzyplatformowych, takich jak DeFi, NFT, DAO i wiele innych. Mimo że CCIP jest nowym narzędziem, wciąż rozwijanym, oferuje ono obiecujące możliwości dla przyszłych innowacji w świecie blockchaina.