Atomic Swap – prosty sposób na międzyłańcuchowe wymiany

Przemek/ 7 listopada, 2022

Handel cyfrowymi aktywami to niezaprzeczalnie jedna z większych aktywności w zdecentralizowanych sieciach. Nie musisz być oczywiście kryptowalutowym “freakiem”, aby tworzyć rozwiązania oparte o blockchain. Dobrze jest natomiast znać mechanizmy, które bazują na tym rynku, ponieważ cała ich masa jest naprawdę pomysłowa i może pomóc w rozwoju innych projektów, a jak wiadomo handel to jeden z największych działów gospodarki. Dzisiaj postaram się przybliżyć mechanizm, który pomaga w bezpieczny sposób dokonać wymiany pomiędzy aktywami znajdującymi się na różnych łańcuchach, a mianowicie mowa o tzw. atomowych wymianach (ang. Atomic Swap). Oprócz teorii, dzisiaj będzie jeszcze trochę praktyki, dlatego pod koniec znajdziesz krótki kod, który zademonstruje Ci działanie tego mechanizmu.

Bitcoin za Ethera, Litecoin za Solanę – pohandlujmy

Wymiana jednej kryptowaluty na drugą to nic nowego, dodatkowo w dzisiejszych czasach jest to bardzo prosta czynność. Zazwyczaj odbywa się to z użyciem standardowych giełd takich jak Binance czy FTX, ale istnieją również zdecentralizowane rozwiązania np. Uniswap. Pierwsze z nich posiadają wszystkie zalety oraz wady zcentralizowanych bytów. Nie będziemy się po raz kolejny zagłębiać w ten temat, bo wiele już zostało o tym powiedziane na łamach tego bloga, ale w kilku słowach możemy się chyba zgodzić, że aplikacje takie są szybkie, proste w obsłudze, ale równocześnie mają pełną kontrolę nad wszystkim tym, co byśmy chcieli w ich obrębie zrobić. Jednak dzięki temu, przy ich pomocy można stosunkowo łatwo stworzyć możliwości wymiany kryptowalut znajdujących się na różnych łańcuchach. Oznacza to, że nie ma problemu, aby jedna osoba mogła wymienić swoje bitcoiny bezpośrednio na ethery lub całkowicie inną kryptowalutę. 

Ponieważ giełda kontroluje klucze prywatne wszystkich użytkowników, to tak de facto handel jest “wirtualny”, ponieważ nie dokonuje się żadna blockchainowa transakcja, a cały stan posiadanych aktywów przez konkretnych użytkowników przechowywany jest w prywatnej bazie danych. Taka sytuacja prowadzi czasem do “zabawnych” konsekwencji, kiedy to użytkownik przy wypłacie może otrzymać więcej niż posiada.

Sprawa ma się nieco inaczej, kiedy spojrzymy na giełdy zdecentralizowane. Wszystkie wymiany w takim rozwiązaniu dokonywane są w obrębie tej samej sieci. Oznacza to, że możemy wymienić np. token Chainlink’a na ten od Binance’a, ale tylko pod warunkiem, że oba znajdują się obecnie na tym samym blockchainie. W poprzednim poście omówiłem temat tzw. mostów, które służą do przenoszenia aktywów między łańcuchami, dlatego jeżeli jeszcze nie znasz tego tematu, to serdecznie polecam.

Kłopot pojawia się w sytuacji, kiedy chcielibyśmy wymienić nasze kryptowaluty (lub jakiekolwiek aktywa) bezpośrednio na te znajdujące się w innej sieci. Dobrym przykładem może być chociażby wymiana bitcoinów na ether. Możemy oczywiście skorzystać z “opakowanych” tokenów (mowa o nich była również w poprzednim poście), które reprezentują oryginalne monety, jednak w wielu sytuacjach chcemy mieć je prawdziwe, a nie jedynie ich substytut. Korzystając jedynie z tradycyjnych zdecentralizowanych giełd, musielibyśmy najpierw przenieść nasze środki z jednego blockchaina na drugi, znaleźć odpowiednią giełdę i następnie dokonać wymiany z naszego opakowanego tokena na ten pożądany. 

Cały proces może i nie jest szczególnie skomplikowany, w szczególności kiedy już odnajdziesz się w zdecentralizowanym świecie, ale może być czasochłonny i dosyć kosztowny. Na szczęście istnieje sprytniejsze rozwiązanie.

Michael Jordan i Cristiano Ronaldo

Rysunek 2. Sportowcy
(źródło: https://www.thesun.co.uk/sport/football/16620878/cristiano-ronaldo-michael-jordan-net-worth-chicago-bulls/)

Mimo że obecnie słowo “atomowe” może budzić w niektórych różne lęki, to w naszym wypadku nie należy się go obawiać. Ale zanim przejdę do detali technicznych, atomowych wymian, to przedstawię analogię, która powinna dobrze zobrazować cały mechanizm.

Tomek i Jacek są wielkimi fanami sportu i każdy z nich posiada różnego rodzaju unikatowe przedmioty. Pewnego dnia pierwszy z nich zaproponował, że z chęcią wymieni swoją oryginalną koszulką Michaela Jordana na strój Cristiano Ronaldo. Jackowi spodobała się ta propozycja, problem w tym, że oboje mieszkają setki km od siebie i trzeba było znaleźć sposób na uczciwą wymianę. Przesyłka zwykłej paczki nie wchodziła w grę, ponieważ nigdy nie wiadomo, czy któraś z osób nie wyśle podróbki.

Kolekcjonerzy wpadli na ciekawy pomysł. Tomek dostał pudełko, do którego mógł włożyć strój MJ’a, po czym zakodował zamek hasłem, które tylko on znał. Posiadał też drugi przedmiot, w którym również ustawił to samo hasło, jednak nie zamykał go, tylko wysłał razem z pierwszym do zainteresowanego wymianą. Teraz Jacek mógł włożyć do drugiego pudełka strój Ronaldo i je zaryglować, ale z wcześniej ustawionym hasłem, którego on nie znał. Ważne było to, że bez otwierania pudełek można było sprawdzić, co się tak naprawdę w nich znajduje, dlatego Jacek miał pewność, że Tomek zapakuje odpowiedni strój. Dodatkowo Jacek bez poznania haseł mógł sprawdzić są one identyczne. Posiadając tę wiedzę, Jacek odesłał swój zapakowany przedmiot do Tomka, a sobie zostawił pudełko z interesującym go strojem.

Na pierwszy rzut oka wydaje się to bez sensu, ponieważ Jacek nie zna hasła do koszulki Jordana, ale jest jeszcze jedna ciekawa właściwość tych pudełek. W momencie, kiedy Tomek otworzy to ze strojem Ronaldo, to Jacek automatycznie dostanie powiadomienie z hasłem, które zostało użyte, przez co sam będzie mógł odkodować swój przedmiot.

Szczerze – mam wrażenie, że nie jest to najlepsza analogia, jaką udało mi się wymyślić na tym blogu, ale mam nadzieję, że i tak dzięki niej będzie Ci łatwiej zrozumieć techniczne detale, do których teraz przejdę.

Wszystko albo nic

Rysunek 3. Niepowiązany żart 🙂
(źródło: https://www.memedroid.com/memes/detail/3310218/insert-something-funny-here)

Zacznijmy od nazwy. Słowo “atomowe” oznacza, że wymiana taka albo musi się dokonać w całości albo w ogóle. Innymi słowy jeżeli ja dostanę Twoje kryptowaluty, to Ty musisz dostać moje.

Pierwszy krokiem takiej wymiany jest ustalenie jej warunków, czyli ile i jakie kryptowaluty będą brały w niej udział. Następnie, po kolei osoby blokują swoje aktywa w aplikacji, przy czym nie musi to być smart kontrakt, ponieważ nie każdy blockchain takimi operuje. Jednak ogólnie przyjęło się określać tego typu rozwiązania kontraktami.

W tym momencie jedna z osób powinna być w stanie odblokować jej należne kryptowaluty. Po wykonaniu tej czynności partner wymiany automatycznie powinien poznać klucz, który pozwoli mu otworzyć drugie “pudełeczko”.

Ważne jest to, że rozwiązania pozwalające na takie wymiany są zbudowane w oparciu o tzw. Hashed Timelock Contract. W skrócie działa to tak, że aktywa znajdujące się w nim są blokowane maksymalnie na z góry określoną ilość czasu. Jeżeli okres ten upłynie, można z powrotem odzyskać swój wkład. Dlatego, gdyby okazało się, że jedna z osób zrezygnowała w trakcie z umówionej wymiany, to nasze środki nie utkną tam na wieki. Będziemy musieli jedynie poczekać jakiś czas, aby je odzyskać.

Jak widzisz, sam koncept jest dosyć prosty, ale zanim przejdę do kodu, to przeanalizujmy jak to rozwiązanie może wyglądać w oparciu o smart kontrakty. W przypadku najprostszego mechanizmu, potrzebujemy dokładnie taki sam kontrakt, na który wgramy na dwa blockchainy np. na Ethereum oraz Polygon. W każdym z nich zapisana musi być informacja o tym, kto uczestniczy w wymianie, w tokenie na konkretnej sieci oraz czasie blokady. Musi też znajdować się gdzieś zahaszowana sekretna fraza, która posłuży jako klucz do odblokowania “pudełka”. Hasło to powinno być oczywiście znane na wstępnie przez jedną z osób, ponieważ w innym wypadku nie uda się dokonać wymiany. 

Należy pamiętać, aby nie przechowywać bezpośrednio samej frazy, ponieważ na blockchainie wszystko jest publiczne i druga osoba mogłaby je od razu sprawdzić. Przy czym powinno ono być automatycznie ujawnione w momencie odebrania kryptowalut przez jedną z osób, aby druga mogła ją poznać i otrzymać należne aktywa. Żeby to było możliwe, to oba kontrakty muszą się opierać o ten sam hash. W uproszczeniu – ten, kto chce zainicjować wymianę, może przekazać zahaszowane hasło drugiej osobie. W praktyce można stworzyć rozwiązanie, które będzie automatycznie tworzyło odpowiednie kontrakty.

Teorie mamy za sobą, dlatego teraz możemy już przejść do konkretów.

Spójrzmy w kod

Rysunek 4. Kodowanie
(źródło: https://medium.com/swlh/learning-to-code-45f7e1657c1e)

Poniżej znajdziesz bardzo prostą implementację pozwalającą dokonać atomowej wymiany. Pomimo, że poniższy smart kontrakt pozwala na poprawne wykonanie atomowych wymian, to nie traktuj tego jako produkcyjnego rozwiązania, ponieważ wymagałoby ono co najmniej kilku usprawnień, które skomplikowałyby całe rozwiązanie, a nie są konieczne, aby przedstawić sam koncept.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
 
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
contract AtomicSwap {
 
   address public immutable owner;
   address public immutable recipient;
   IERC20 public immutable token;
   uint256 public immutable timelock;
   bytes32 public immutable secretHash;
 
   bool public opened;   
   uint256 public amount;
   uint256 public expirationTime;
   string public secret;
 
   constructor(address _recipient, IERC20 _token, bytes32 _secretHash) {
       owner = msg.sender;
       recipient = _recipient;
       token = _token;
       timelock = 10 minutes;
       secretHash = _secretHash;
   }
 
   function openSwap(uint256 _amount) external {      
       require(msg.sender == owner, "You are not the owner.");
       require(!opened, "The swap has been already opened.");
       opened = true;
       expirationTime = block.timestamp + timelock;
       amount = _amount;
       token.transferFrom(msg.sender, address(this), _amount);
   }
  
 
   function closeSwap(string memory _secret) external {
       require(msg.sender == recipient, "You are not the recipient.");
       require(opened, "The swap hasn't been opened yet.");
      
       bytes32 hash = keccak256(abi.encodePacked(_secret));
       require(hash == secretHash, "Wrong secret.");
     
       secret = _secret;
       token.transfer(recipient, amount);
   }
 
 
   function refund() external {
require(msg.sender == owner, "You are not the owner.");
       require(expirationTime <= block.timestamp, "The swap hasn't expired yet.");
       token.transfer(owner, amount);
   }
}

Jak widzisz kodu nie ma dużo, dlatego przejdźmy szybko przez to, co w nim się dzieje. Na samym początku znajduje się kilka stałych/zmiennych, które będą przechowywały informację o:

  • pierwotnym właścicielu tokenów (owner)
  • odbiorcy (recipient)
  • tokenie, którego dotyczy wymiana (token)
  • długości czasu blokady tokenów (timelock)
  • zahaszowanym tajnym haśle (secretHash)
  • tajnym haśle (secret)
  • końcu czasu blokady tokenów (expirationTime)
  • ilości tokenów na wymianę (amount)
  • oraz o tym, czy wymiana została już otwarta (opened).

Konstruktor pomoże nam w ustawieniu kilku wartości. Reszta pozostaje z wartościami domyślnymi, które zostaną nadpisane w trakcie pozostałych operacji.

Pierwsza z zaimplementowanych funkcji służy do otwarcia wymiany. Wywołana może być jedynie raz i to tylko przez właściciela. Posłuży ona do przesłania zadeklarowanej liczby tokenów do kontraktu oraz odpowiedniego ustawienia niektórych zmiennych. Należy pamiętać, że przed wykonaniem tego kawałka kodu, należy najpierw wywołać funkcję “approve” na kontrakcie tokena, który wykorzystujemy do tej wymiany. Jeżeli nie wiesz, do czego służy ta funkcja, to zerknij do tego posta.

Kolejna funkcja służy do odebrania tokenów przez partnera wymiany. Jak widzisz wymaga ona przekazania sekretnego hasła, które następnie jest haszowane, a wynik porównywany jest z przechowywaną w kontrakcie wartością. Jeżeli hasze się zgadzają, to hasło zostanie zapisane, a tokeny przesłane na odpowiedni adres.

Zadaniem ostatniej funkcji jest odzyskanie środków z kontraktu, w przypadku, gdy odbiorca nie zamknie wymiany i wygaśnie czas blokady tokenów.

Proponuję Ci teraz wgrać kontrakty na dwie różne sieci testowe i sprawdzić, czy wszystko dobre zadziała. Jeżeli nie wiesz jak się do tego zabrać, to wszystkie niezbędne informację znajdziesz w tym poście

Dla ułatwienia podam jeszcze kroki, którymi możesz się kierować, aby zasymulować wymianę:

  1. Stwórz token ERC20 na pierwszej sieci (lub skorzystaj z tych, które już posiadasz)
  2. Wgraj kontrakt AtomicSwap na pierwszą sieć
  3. Wywołaj funckję “approve” podając liczbę tokenów i adres kontraktu AtomicSwap.
  4. Otwórz wymianę
  5. Z drugiego konta zrób to samo na drugiej sieci
  6. Pozamykaj wymiany
  7. Jeżeli wszystko poszło zgodnie z planem, to tokeny powinny znaleźć się na odpowiednich kontach

Trzeba przyznać, że wykonanie tych kroków z użyciem Remix’a nie jest najwygodniejszą metodą, ale w praktyce można stworzyć do tego np. aplikację webową, która zdecydowanie wszystko ułatwi. Zachęcam Cię jeszcze do pobawienia się nieco kodem i stworzenia np. możliwości tworzenia wymian bez konieczności wgrywania za każdym razem nowego kontraktu.

Liczę, że udało mi się przybliżyć Ci kolejny, w mojej opinie niezwykle sprytny blockchainowy mechanizm. Interoperacyjność łańcuchów to niezwykle ważny temat, a atomowe wymiany to coś, co zdecydowanie poprawia tę cechę. Mam nadzieję, że jeszcze nie zanudziłem Cię zdecentralizowanymi tematami i zobaczymy się za tydzień, kiedy poruszymy kolejny temat, którym będzie… w sumie jeszcze się okaże co 🙂

Share this Post
Subscribe
Powiadom o
guest
0 komentarzy
Inline Feedbacks
View all comments