Loteria na blockchainie – tworzymy pierwszy smart kontrakt

Przemek/ 18 lipca, 2022

Znajomość pojęć i koncepcji blockchainowych jest bardzo istotna, ale najfajniejsze jest wykorzystanie ich w praktyce. Dlatego dzisiaj zajmiemy się stworzeniem jednego, dosyć prostego smart kontraktu. Dowiesz się czym jest Remix i czemu jest to dobry wybór na start, doładujesz środkami swój portfel, a na koniec wrzucisz stworzony smart kontrakt na blockchain i będziesz mógł pochwalić się swoim kodem całemu światu:)

Jeżeli planujesz zostać programistą Web 3.0, ale chciałbyś zajmować się stroną frontendową, to i tak warto, żebyś zapoznał się z wspomnianymi mechanizmami. Pozwoli Ci to lepiej zrozumieć, co takiego dzieje się pod spodem Jeśl jesteś osobą nietechniczną i nie planujesz kariery blockchain dewelopera, to również możesz wyciągnąć coś dla siebie z tego wpisu. Gdy lepiej zrozumiesz, jak działają smart kontrakty, to łatwiej będzie Ci zidentyfikować blockchainowe oszustwa.

Dzisiejszy wpis wyszedł nieco długi, jednak chciałem szczegółowe opisać niektóre mechanizmy, żeby w przyszłości nie musieć tego robić ponownie. Dlatego momentami może on przypominać przewodnik krok po kroku, co powinno ułatwić wejście w temat. Także bez zbędnego gadania, czas zabrać się do roboty!

Przygotujmy się!

Rysunek 1. Przygotuj się
(źródło: https://imgflip.com/i/2z1y94)

Podczas dzisiejszego tworzenia smart kontraktu będziesz potrzebował kilku narzędzi. Zanim przejdziemy do kodu, zajmiemy się ich konfiguracją.

Ostateczne będziemy chcieli, aby nasz smart kontrakt znalazł się na blockchainie, dlatego potrzebujemy aplikacji, która stworzy w naszym imieniu transakcję. Wykorzystamy do tego Metamaska. Jeżeli jeszcze nie miałeś okazji korzystać z tej aplikacji, to w tym wpisie opisałem ją ze szczegółami.

Następnie potrzebujesz założyć konto na etherscanie. Jest to aplikacja, która pozwala przeglądać, co takiego dzieje się na blockchainie Ethereum. Konto będzie niezbędne do wygenerowania API Key, które przyda się nam do weryfikacji kodu.
Potrzebujemy jeszcze narzędzia, które umożliwi nam tworzenie kodu. Skorzystamy z Remixa. Jest to online IDE (Integrated Development Environment), które pozwala w łatwy sposób tworzyć smart kontrakty, bez potrzeby instalowania czegokolwiek na swoim komputerze. Gdy już będziesz tworzył zaawansowane rozwiązanie, składające się z wielu kontraktów, to zdecydowanie lepszą opcją będzie użycie narzędzia o nazwie Hardhat, jednak na start lepiej skorzystać z czegoś prostszego. Żeby sprawnie poruszać się po tym narzędziu, przejdźmy razem przez jego zakładki.

Remix – szybkie wprowadzenie

Pierwszym, na co natkniesz się po wejściu w Remixa, będzie Twój workspace. Możesz zapoznać się z kodem przykładowych smart kontraktów umieszczonych w folderze “contracts”. To właśnie tam umieścimy kod naszego rozwiązania.

Rysunek 2. Remix – workspace

Kolejną zakładką jest “Solidity compiler” (trzecia od góry). W tym miejscu możesz ustawić wersję solidity, którą będzie wykorzystywał kompilator. Na chwilę pisania tego wpisu rekomendowaną wersją jest 0.8.7, dlatego jeżeli domyślnie masz ustawioną inną, to zmień właśnie na tą wspomnianą. Przy okazji polecam zaznaczyć opcję “Auto compile”, wtedy kod będzie się automatycznie kompilował.

Rysunek 3. Remix – compiler

Następnie przejdźmy do “Deploy & run transactions”. Jest to miejsce, które pomoże nam wgrać smart kontrakt oraz wywoływać jego funkcję.

Rysunek 4. Remix – deploy & run transations

Pole environment to blockchain, na który będziemy “wrzucać” kod. Domyślnie ustawiona jest opcja “JavaScript VM (London)”. Jest to imitacja blockchaina, która w łatwy sposób pozwala testować stworzone smart kontrakty. Będziemy dzisiaj z niej korzystać, ale również posłużymy się opcją “Injected Web3”, która połączy Remixa z Metamaskiem i pozwoli nam na wgranie kodu na prawdziwy blockchain.

Poniżej znajdziesz pole o nazwie “Account”. Reprezentuje ono konto, przy pomocy którego będzie wykonywana najbliższa transakcja. Zapewne widzisz, że do wyboru masz kilku użytkowników, a na każdym z nich znajduje się po 100 etheru. Niestety nie zostałeś właśnie milionerem, ponieważ jest to ether na imitowanym blockchainie. Innymi słowy nie ma żadnej realnej wartości:)

Kolejno mamy pole “Gas limit”, którym nie będziemy się teraz zajmować oraz “Value”, które przyda nam się do wysłania etheru. Poniżej znajdziesz spis kontraktów, które możesz wgrać na blockchain. Jeżeli widzisz pustą listę, to wróć do workspace’u i otwórz któryś z przykładowych plików. Po tym wybrany kontrakt powinien się pojawić we wspomnianym polu. Spróbuj teraz go wgrać przy pomocy przycisku “deploy”.

W konsoli powinieneś zobaczyć logi, mówiące o pomyślnej transakcji oraz na dole po lewej pojawią się opcje do interakcji z kontraktem. Aby zapoznać się lepiej z narzędziem, polecam przeklikać kilka funkcji.

Rysunek 5. Remix – logi

Przed przejściem dalej zróbmy jeszcze jedną rzecz, a mianowicie zainstalujmy plugin, który pozwoli nam w łatwy sposób zweryfikować kontrakty na etherscanie. Posłuży do tego “Plugin manager” (druga opcja od dołu).

Rysunek 6. Remix – plugin manager

Wyszukaj wtyczkę o nazwie “Etherscan – Contract Verification” i aktywuj ją. Po tej akcji powinna pojawić Ci się dodatkowa zakładka, ale zajmiemy się nią później.

Ustalmy reguły

Pierwszym smart kontraktem, który będziemy chcieli zaimplementować, będzie prosta loteria. Zasady są następujące:

  1. Loteria rozpoczyna się w trakcie wgrania smart kontraktu na blockchain.
  2. W kontrakcie zdefiniowany jest czas trwania loterii.
  3. Aby wziąć udział w loterii, użytkownik musi kupić bilet, którego cena jest niezmienne i zdefiniowana w smart kontrakcie.
  4. Płatność za bilet dokonywana będzie przy pomocy etheru.
  5. Użytkownik może kupić dowolną liczbę biletów.
  6. Po upływie czasu trwania loterii, dowolny użytkownik może wyłonić zwycięzcę, a ten automatycznie otrzyma w nagrodę wszystkie zgromadzone środki.

Z grubsza to by było na tyle. Nie ma tego wiele, ale stworzony kod pozwoli Ci zapoznać się z kilkoma mechanizmami języka Solidity.

Pokodujmy!

Wspólne tworzenie i omówienie kodu jest zdecydowanie łatwiejsze, kiedy całość robiona jest w formie wideo np. na platformie YouTube. Póki co nie posiadam swojego kanału, ale nie wiadomo nigdy, co przyniesie przyszłość. Dlatego musimy poradzić sobie z tym inaczej:)

Nie widzę sensu rozciągać tego wpisu i tworzyć linijkę po linijce, dlatego wkleję od razu całe rozwiązanie, a poniżej znajdziesz dokładne omówienie tego, co się w nim dzieję. Ponieważ jest to pierwszy raz na tym blogu, kiedy zajmujemy się kodem, dlatego omówię każdą linijkę. Nie będę zagłębiał zbyt szczegółowo we wszystkie zagadnienia, ponieważ każde z nich mogłoby wymagać osobnego wpisu. Jeżeli coś w tej chwili nie jest jasne dla Ciebie z perspektywy samego języka lub składni, to odsyłam Cię do dokumentacji Solidity. Cały kod należy umieścić w nowym pliku w folderze “contracts”.

//SPDX-License-Identifier: MIT
pragma solidity 0.8.7;
 
contract Lottery {
   event TicketPurchased(address who);
   event WinnerPicked(address who, uint256 reward);
  
   uint256 private immutable i_ticketPrice;
   uint256 private immutable i_endTime;
  
   address[] players;
  
   constructor() {
       i_ticketPrice = 1e18; //1000000000000000000 = 1 ETH
       i_endTime = block.timestamp + 180;
   }
 
   function buyTicket() external payable {
       require(block.timestamp <= i_endTime, "Lottery has ended");
       require(msg.value == i_ticketPrice, "Wrong purchase");
      
       players.push(msg.sender);
 
       emit TicketPurchased(msg.sender);
   }
 
   function pickWinner() external {
       require(block.timestamp > i_endTime);
 
       uint256 winnerIndex =  uint256(keccak256(abi.encodePacked(block.timestamp, players.length, block.number))) % players.length;
       address payable winner = payable(players[winnerIndex]);
       uint256 reward = address(this).balance;
 
       (bool sucess, ) = winner.call{value: reward}("");
 
       require(sucess, "Reward transfer failed");
 
       emit WinnerPicked(winner, reward);
  
   }
 
   function getTicketPrice() external view returns(uint256) {
       return i_ticketPrice;
   }
 
   function getEndTime() external view returns(uint256) {
       return i_endTime;
   }
 
   function getPlayers() external view returns(address[] memory) {
       return players;
   }
}

Rozpoczynamy od zadeklarowania licencji, pod którą wystawiamy nasz kod. Mimo że wszystko na blockchainie jest publiczne, to nie zawsze chcemy, aby ktoś kopiował nasz pomysł. Z tego powodu możemy umieścić informację o odpowiedniej licencji. Nie uchroni to nas oczywiście od samego skopiowania treści, ale w razie takiej sytuacji, pomoże nam dochodzić swoich praw. Spis powszechnie używanych licencji znajdziesz tutaj

Kolejne linijka, to deklaracja wersji Solidity która będzie używana przez kontrakt. W naszym przypadku jest to 0.8.7. Po tych krokach możemy stworzyć ramy naszego kontraktu. Robimy to przy pomocy słowa kluczowe “contract” oraz podania swojej nazwy.

Na samym początku zadeklarujemy dwa eventy. Nie wchodząc w szczegóły, użyjemy ich później do zapisania zdarzenia związanego z zakupem biletu oraz wyborem wygranego. 

Kolejne kilka linijek to pola, które będą przechowywały dane dotyczące ceny biletu, czasu zakończenia loterii oraz graczy, którzy biorą w niej udział. Nie zaprzątaj sobie głowy słowem “immutable” oraz przedrostkami “i_” przed nazwą tych zmiennych. Również słówko “private” działa nieco inaczej niż w przypadku standardowych języków programowania. Omówimy ten temat szczegółowo innym razem.

Następnie możemy stworzyć konstruktor. Wykorzystamy go do ustawienia parametrów loterii. Sam konstruktor jest specyficzną funkcją, która wywoływana jest jedynie raz, podczas tworzenia smart kontraktu. Zwraca ona kod bajtowy, który zostanie umieszczony na blockchainie.

Do ustalenia czasu zakończenia loterii wykorzystamy wartość ”block.timestamp”. Dzięki niej otrzymamy informację o czasie, w którym smart kontrakt został wgrany. Dodając do niego wartość 180, ustalimy długość trwania loterii na 3 minuty. Powinno to być wystarczające do naszej zabawy. Jeśli chcesz sparametryzować tą wartość, to śmiało:)

Dla ułatwienia również cenę biletu ustawimy na stałe i wynosić ona będzie 1 ETH. Może Cię dziwić zapis “1e18”, który odzwierciedla liczbę 1000000000000000000. Ponieważ ether jest podzielny do 18 miejsc po przecinku, to ta właśnie liczba odzwierciedla jedną całą jednostkę.

Teraz możemy przejść do funkcji umożliwiającej zakup biletu. Zgodnie z założeniami gracz będzie musiał uiścić opłatę w formie etheru. Żeby to było możliwe, to funkcja musi być oznaczona jako “payable”. Drugi modyfikator o nazwie “external” oznacza, że wywołać ją będzie można jedynie z zewnątrz kontraktu. Ponieważ nasz kontrakt nie będzie potrzebował z niej korzystać wewnętrznie, dlatego będzie to odpowiednie rozwiązanie. W przyszłości pomówimy sobie więcej o modyfikatorach.

Pierwsze dwie linijki funkcji odpowiadają za sprawdzenie czy loteria wciąż trwa oraz czy wysłany ether odpowiada cenie biletu. Używamy tutaj “require”, które jako pierwszy parametr przyjmuje warunek, a jako drugi wiadomość, która będzie przekazana razem z błędem, w razie niespełnienie wymagań.

Aby pobrać ilość etheru, który został wysłany, potrzebujemy odwołać się do obiektu “msg” i jego pola “value”. Również dodając nowego gracza skorzystamy z tego samego obiektu, tylko tym razem pobierając wartość z pola “sender”. Jest to adres, który wywołał funkcję.

Na samym końcu emitujemy event, w którym zawrzemy informacje dotyczące zakupu biletu.

Możemy teraz przejść do drugiej funkcji, czyli wyboru zwycięzcy. Na początku musimy sprawdzić, czy czas loterii dobiegł końca. Jeśli tak, to możemy wylosować zwycięzcę. Do utworzenia losowej liczby wykorzystamy wbudowaną funkcję haszującą “keecak256”. Przy jej pomocy stworzymy ciąg znaków, który będzie odzwierciedlał pewną liczbę. Jeżeli nie pamiętasz jak działa haszowanie, to przedstawiłem tę mechanizm w jednym z poprzednich wpisów. Do otrzymania ciągu wykorzystamy obecny czas bloku, numer bloku oraz liczbę graczy. Na otrzymanym wyniku wykonamy operację modulo z liczby graczy i tym sposobem otrzymamy losowy indeks zwycięzcy.

Teraz możemy pobrać adres zwycięzcy. Podobne jak w przypadku funkcji, która przyjmuje ether, również adres, na który chcemy przetransferować środki oznaczamy jaka “payable”. 

Zostaje nam jeszcze pobranie wartości nagrody oraz wysłanie jej do wygranego. Zrobimy to przy pomocy funkcji “call”, którą wywołamy na adresie i przekażemy jej całe saldo kontraktu. Jeżeli transfer się udał, to emitujemy event mówiący o zakończeniu aukcji.

Dopiszmy jeszcze kilka funkcji, które ułatwią nam przeglądanie danych zapisanych w kontrakcie. I to by było na tyle, jeśli chodzi o kodowanie. Teraz możemy zagrać w grę.

Sprawdźmy, czy to działa

Zanim wgramy smart kontrakt na prawdziwy blockchain, sprawdźmy najpierw czy wszystko działa zgodnie z założeniami. Użyjemy do tego JavaScriptowej imitacji blockchaina. Wiesz już jak wgrać tam kontrakt, dlatego nie będę ponownie omawiał tego procesu.

Jeżeli cena biletu wydaje się za wysoka, a czas za krótki na zabawę, to śmiało zmień te wartości w konstruktorze.

Ponieważ zakup biletu wymaga wysłania etheru, musisz tę wartość ustawić w polu value. Zmień “Wei” na “Ether”, wtedy będziesz mógł podać wartość 1.

Rysunek 7. Remix – wgranie kontraktu

Szczegóły każdej transakcji widoczne są w dolnej konsoli. Po zakupie kilku biletów i odczekaniu odpowiedniej ilości czasu, czas wybrać zwycięzcę. W moim przypadku był to użytkownik o adresie “0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB”. 

Rysunek 8. Remix – koniec loterii

Skoro wszystko działa jak należy, to możemy przejść do ostatniego kroku, czyli wgrania smart kontraktu na prawdziwy blockchain. Przy czym użyjemy do tego jednego z testowych środowisk, żeby nie ponosić prawdziwych kosztów. Składać się to będzie z trzech kroków. Po pierwsze musimy doładować środki na naszym portfelu, ponieważ każda transakcja kosztuje. Następnie wrzucimy sam kontrakt, a na końcu zweryfikujemy jego kod i sprawdzimy czy wszystko jest widoczne na etherscanie.

Pokażmy się światu

Do pozyskania środków na testowym blockchainie skorzystamy z serwisu dostarczonego przez Chainlinka. Po wejściu wystarczy, że połączysz się swoim portfelem z aplikacją, wypełnisz CAPTCH’e i naciśniesz przycisk “Send request”. Możesz też odznaczyć checkbox z tokenami LINK, ponieważ nie będą nam dzisiaj potrzebne. Jednak upewnij się najpierw czy w Metamasku wybrałeś sieć “Rinkeby”. Jeśli tak, to po chwili 0.1 ETH powinno znaleźć się na Twoim portfelu.

Teraz jesteśmy gotowi do wgrania kontraktu. Tym razem w panelu “Deploy & Run Transaction” w polu Environment wybierz opcję ”Injected Web3” i połącz swojego Metamaska z Remixem. W ten sam sposób co poprzednio, możesz teraz wgrać swój smart kontrakt. Będziesz musiał zatwierdzić transakcję w Metamasku oraz odczekać chwilę zanim dostaniesz informację o pomyślnie wykonanej transakcji. Ponieważ działamy teraz na prawdziwym blockchainie, musimy poczekać aż zostanie wykopany blok. 

Kiedy transakcja dobiegnie końca, skopiuj adres kontraktu i wejdź na Etherscan dla sieci Rinkeby. Znajdziesz go pod tym adresem. Teraz znajdź swój kontrakt. Jeżeli wyszukiwarka nic nie zwraca, poczekaj chwilę, ponieważ trwa synchronizacja aplikacji z blockchainem. Jeżeli wszystko poszło pomyślnie, to powinieneś ujrzeć widok podobny do tego poniżej.

Rysunek 9. Etherscan – kontrakt

Póki co nie widać jeszcze kodu naszego kontraktu, a jedynie ciąg znaków, który reprezentuje wgrany kod bajtowy. Sprawmy więc, żeby wszystko było czytelne dla każdego użytkownika, a pomoże nam w tym wtyczka, którą zainstalowaliśmy na samym początku. 

Wymaga ona podania API Key, który możesz wygenerować logując się na swoje konto w Etherscanie. Wejdź w zakładkę “API Keys” i utwórz nowy klucz. Teraz możesz go wprowadzić do wtyczki, wybrać z listy swój kontrakt, podać jego adres na sieci Rinkeby i nacisnąć przycisk “Verify Contract”. Po pomyślnym przejściu weryfikacji, może się okazać, że wciąż kod jest nieczytelny. Ponownie będziesz musiał chwilę poczekać, zanim Etherscan odświeży swój stan. Po krótszej bądź dłuższej chwili, kod bajtowy powinien zamienić się w czytelny tekst Solidity.

Rysunek 10. Etherscan – zweryfikowany kontrakt

Czy aby wszystko zrobiliśmy jak należy?

Za nami całkiem spory kawałek dobrej roboty. Podczas dzisiejszego wpisu dowiedziałeś się nie tylko jak stworzyć smart kontrakt, ale również poznałeś środowisko Remix, doładowałeś swój portfel oraz wgrałeś smart kontrakt na realny blockchain. Jednak szczerze powiedziawszy w naszym kodzie znajduje się jeden duży problem. I nie mam tutaj na myśli sparametryzowania konstruktora czy utworzenia bardziej optymalnych funkcji. To też są ważne zagadnienia, ale w tej chwili nie ma sensu ich poruszać. Natomiast tym, co powinno nas zastanowić, jest sposób w jaki generujemy losową liczbę.

Jak już sam się przekonałeś, wszystko na blockchainie jest publiczne. Każda osoba może zatem sprawdzić, jak wygląda Twój smart kontrakt. Z tego powodu generowanie losowej liczby nie powinno odbywać się wewnątrz kontraktu. Jeżeli zostawimy taki kod, to dowolny użytkownik może tak manewrować funkcjami w smart kontrakcie, aby uzyskana liczba odpowiadała jego pozycji w tabeli. Jak zatem poradzić sobie z tym problemem? Pomogą nam w tym tzw. “wyrocznie”. Czym one są i jak działają opowiem kolejnym razem:)

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