Stwórzmy swoje krypto

Przemek/ 8 sierpnia, 2022

Jeżeli chciałbyś stworzyć klasyczny token ERC20, to w praktyce nie musiałbyś posiadać żadnej blockchainowej wiedzy. Istnieje wiele portali, które zrobią Ciebie wszystko. Jednak skoro znalazłeś się na tym blogu, to pewnie jesteś osobą, która chce dowiedzieć się więcej.

 Dlatego dzisiaj sami zaimplementujemy nową kryptowalutę, wykorzystując do tego Remixa i Solidity. Z tego wpisu dowiesz się, co zawiera standard ERC20, jak go zaimplementować oraz co należy zrobić, aby Twoje krypto wyświetliło się w Metamasku.

Co w trawie piszczy – standard ERC20

Rysunek 1. Co w trawie piszczy
(źródło: https://www.cineman.pl/film/vod.1950-co-w-trawie-piszczy)

Poprzednim razem wspomniałem o standardzie ERC20, jednak nie zgłębiałem jego technicznych detali. Teraz wreszcie przyszedł na to czas. 

Interfejs ten definiuje dziewięć funkcji oraz dwa eventy, które muszą być zaimplementowane w każdym kontrakcie,pełniącym rolę tokena ERC20. Wymagania opisane są na oficjalnej stronie, ale żeby było łatwiej, to omówię teraz poszczególne funkcje.
Każdy token ERC20 musi posiadać swoją nazwę oraz symbol, inaczej zwany tickerem. Jest to kilkuliterowy skrót reprezentujący daną kryptowalutę. Wartości te muszą być zwracane przez następujące funkcje:

function name() public view returns (string)
function symbol() public view returns (string)

Token powinien posiadać również parametr mówiący o jego sumarycznej, dostępnej ilości. Warto zaznaczyć, że wartość ta nie musi być maksymalną liczbą tokenów, które kiedykolwiek powstaną. Każdy token można zaimplementować w taki sposób, aby było możliwe zarówno stworzenie nowych jednostek, jak i zniszczenie już istniejących. ERC20 nie stawia co do tego żadnych wymagań. Funkcja, która udostępnia tę informację ma następującą sygnaturę:

function totalSupply() public view returns (uint256)

Kolejnym parametrem jest wartość mówiąca, jak bardzo podzielny jest token. Tak samo jak 1 USD jest równy 100 centom, również kryptowaluta może posiadać mniejsze jednostki. Dla przykładu ether, który akurat nie jest tokenem ERC20, można podzielić aż do 18 miejsc po przecinku. W ten sposób otrzymamy 1 WEI. Przy czym wartość tego parametru służy jedynie po to, by usprawnić użyteczność tokena. Dla przykładu jeśli posiadałbyś 1000 jednostek, których podzielność wynosi 2, to większość aplikacji pokaże Ci, że na koncie masz 10 tokenów. Jeżeli teraz tego nie rozumiesz, to nie przejmuj się, zobaczysz jak to działa w dalszej części wpisu:) Do pobrania tej wartości służy funkcja:

function decimals() public view returns (uint8)

Kolejną istotną informacją jest ilość środków przypisana do poszczególnego konta, inaczaj zwana bilansem.

function balanceOf(address _owner) public view returns (uint256 balance)

Ostatnią rzeczą, którą musi zwracać ERC20 jest informacja o ilości środków, których ktoś może użyć w czyimś imieniu. Innymi słowy możesz dać komuś kieszonkowe:)

function allowance(address _owner, address _spender) public view returns (uint256 remaining)

ERC20, oprócz udostępniania informacji, definiuje metody, które służą do wykonywania konkretnych operacji. Pierwsza z nich, to funkcja odpowiedzialna za transfer środków z portfela osoby, która ją wywołuje na podany adres, a jej sygnatura to:

function transfer(address _to, uint256 _value) public returns (bool success)

Standard definiuje również metodę, która pozwala wysłać środki w czyimś imieniu:

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)

Ostatnią niezbędna funkcją, jest ta ustawiająca wspomniane kieszonkowe:

function approve(address _spender, uint256 _value) public returns (bool success)

Powiedziałem wcześniej o dwóch eventach, które również muszą być zaimplementowane. Oto i one:

event Transfer(address indexed _from, address indexed _to, uint256 _value)
event Approval(address indexed _owner, address indexed _spender, uint256 _value)

Pierwszy z nich powinien być “rzucony” w momencie dowolnego transferu, drugi natomiast podczas przydzielania środków dla innego konta.

Standard mamy już za sobą, więc możemy przejść do “mięsa” i pobawić się w kodowanie 🙂

Raz, dwa, trzy, ERC implementujesz Ty

Tak jak było to w poprzednich wpisach, gdzie dotykaliśmy kodu, tak i tutaj najpierw zaprezentuję kod, a później go dokładnie omówię.

//SPDX-License-Identifier: MIT
pragma solidity "0.8.7";
 
contract Token {
   event Transfer(address indexed _from, address indexed _to, uint256 _value);
   event Approval(
       address indexed _owner,
       address indexed _spender,
       uint256 _value
   );
 
   uint8 private s_decimals;
   uint256 private s_totalSupply;
   string private s_name;
   string private s_symbol;
 
   mapping(address => uint256) private s_balances;
   mapping(address => mapping(address => uint256)) private s_allowances;
 
   constructor(
       uint8 _decimals,
       uint256 _totalSupply,
       string memory _name,
       string memory _symbol
   ) {
       s_decimals = _decimals;
       s_totalSupply = _totalSupply;
       s_name = _name;
       s_symbol = _symbol;
 
       s_balances[msg.sender] = _totalSupply;
   }
 
   function transfer(address _to, uint256 _value)
       public
       returns (bool success)
   {
       require(s_balances[msg.sender] >= _value, "Not enough balance");
       s_balances[msg.sender] -= _value;
       s_balances[_to] += _value;
 
       emit Transfer(msg.sender, _to, _value);
 
       return true;
   }
 
   function transferFrom(
       address _from,
       address _to,
       uint256 _value
   ) public returns (bool success) {
       require(
           s_allowances[_from][msg.sender] >= _value,
           "Not enough allowance"
       );
       require(s_balances[_from] >= _value, "Not enough balance");
 
       s_balances[_from] -= _value;
       s_balances[_to] += _value;
 
       emit Transfer(_from, _to, _value);
 
       return true;
   }
 
   function approve(address _spender, uint256 _value)
       public
       returns (bool success)
   {
       s_allowances[msg.sender][_spender] = _value;
       emit Approval(msg.sender, _spender, _value);
 
       return true;
   }
 
   function name() public view returns (string memory) {
       return s_name;
   }
 
   function symbol() public view returns (string memory) {
       return s_symbol;
   }
 
   function decimals() public view returns (uint8) {
       return s_decimals;
   }
 
   function totalSupply() public view returns (uint256) {
       return s_totalSupply;
   }
 
   function balanceOf(address _owner) public view returns (uint256 balance) {
       return s_balances[_owner];
   }
 
   function allowance(address _owner, address _spender)
       public
       view
       returns (uint256 remaining)
   {
       return s_allowances[_owner][_spender];
   }
}

Na początku kontraktu znajdziesz definicję dwóch wspomnianych eventów. Poniżej zadeklarowane są pola, które przechowują wartości wymagane przez standard ERC20.

Następnie zdefiniowany jest konstruktor, przy pomocy którego będziemy mogli ustawiać takie parametry jak: podzielność (_decimals), tokeny w obiegu (_totalSupply), nazwę (_name) oraz symbol (symbol). Niezwykle ważna jest również ostatnia linia konstruktora, która przypisuje wszystkie tokeny do adresu portfela wgrywającego kontrakt. W naszym przypadku, gdyby zabrakło tej linijki, to stworzona kryptowaluta byłaby bezużyteczna, ponieważ nikt nie miałby dostępu do żadnych środków. To tak jakby utworzyć bank, w którym zdeponowalibyśmy 100 mln złotych, zamknęli je w niezniszczalnym sejfie, do którego nikt nie miałby dostępu. Fajnie, że są tam pieniądze, ale co z tego, kiedy żadna osoba nie może z nimi nic zrobić:)

Pierwsza funkcja, jaka został zaimplementowana to ta, która odpowiada za przesyłanie tokenów między adresami. Na początku sprawdzamy, czy użytkownik posiada wystarczającą ilość środków i jeśli tak, to zmieniamy bilans poszczególnych kont. Następnie emitujemy event “Transfer” oraz zwracamy wartość “true”, zgodnie z wymaganiami ERC20.

Kolejna funkcja jest bardzo podobna, z tą różnicą, że dodatkowo sprawadzmy czy konto, które próbuje zrobić transfer środków, ma do nich uprawnienia. Jest to wspomniane wcześniej kieszonkowe. Jeśli tak, to analogicznie do metody “transfer” wykonujemy kolejne operacje, podając odpowiednie parametry.

Przechodząc dalej, znajdziesz funkcję “approve”, dzięki której użytkownik będzie mógł przydzielać do użytku swoje tokeny innym adresom. Oprócz ustawienia wartości, metoda ta emituje event “Approved” oraz zwraca wartość “true”.

Poniżej znajdują się już tylko funkcje, które zwracają poszczególne wartości z kontraktu. I to by było na tyle. Jak widzisz, kilkanaście linijek kodu wystarczy, żeby stworzyć nową kryptowalutę.

Metamasku drogi, pokaż mi moje krypto

Skoro wszystko już gotowe, to zostało nam jeszcze wgranie kodu na blockchain, zweryfikowanie go oraz dodanie tokena do naszego portfela. Pierwsze dwa kroki powinny być już dla Ciebie jasne, dlatego nie będę ich po kolei tłumaczył i skupię się na tym ostatnim. Ważne jest jedynie, żebyś wgrał kontrakt na sieć testową, a nie korzystał z imitacji blockchaina, ponieważ inaczej nie połączysz tokena z metamaskiem. Jeżeli nie wiesz jeszcze jak przejść ten proces, to szczegółowe instrukcje znajdziesz w tym i tym poście. Dodam jeszcze, że warto ustawić “decimals” na 18. Co prawda w tej chwili nie ma to większego znaczenia, ale jeśli chciałbyś stworzyć dodatkowe rozwiązanie, w którym umożliwisz zakup tokena przy pomoc etheru, to takie ustawienie zdecydowanie ułatwi konwersję, ponieważ obie kryptowaluty będą miały taką samą podzielność. W takim przypadku ustaw również odpowiednie “totalSupply”. Przykładowo, jeżeli chciałbyś, aby w portfelu widniała wartość 1 mln tokenów, to parametr ten musisz ustawić na 1000000000000000000000000 (24 zera = 1000000 * 10^18).

Rysunek 4. Deploy

Po wgraniu smart kontraktu skopiuj jego adres i przejdź do swojego Metamaska. W zakładce “Assets”, na samym dole znajdziesz opcję “Import tokens”. Wybierz ją.

Rysunek 5. Metamask Assets

Po wprowadzeniu adresu tokena, reszta danych powinna zostać zaczytana automatycznie. Gdyby z jakichś względów tak się nie stało, uzupełnij je manualnie.

Rysunek 6. Dodawanie tokena

Następnie przeklikaj kolejne kroki i gotowe. Token powinien być widoczny w Twoim portfelu. Możesz w tej chwili korzystać z niego tak, jak z każdego innego krypto. Oczywiście nie ma on żadnej wartości rynkowej, ale zawsze możesz go przesłać swojemu znajomemu i wmówić mu, że jest to krypto, które zrewolucjonizuje świat ;).

Rysune k 7. Dodany token

W tym miejscu mógłbym zakończyć wpis, ponieważ przeszliśmy przez wszystkie zaplanowane kroki. Ale jest jeszcze jedna rzecz, o której warto wspomnieć, zanim się pożegnamy.

Nie wymyślaj koła na nowo

Implementując po raz pierwszy swój token ERC20, warto jest zrobić wszystko samemu. Pozwoli to zrozumieć dokładnie co dzieje się pod spodem. Natomiast bez sensu byłoby robić to za każdym razem, kiedy potrzebujemy zaimplementować nowe krypto. Tym bardziej nie ma to sensu ze względu na potencjalne błędy, które moglibyśmy popełnić. Dlatego dobrze by było, jakby istniało rozwiązanie, które poda nam gotowy kod, a my jedynie go dostosujemy pod siebie. Na szczęście coś takiego istnieje.

OpenZeppelin dostarcza zbiór smart kontraktów, z których możemy skorzystać implementując swoje rozwiązania. Każda z ich implementacji jest porządnie przetestowana i zaudytowana, co znacznie zmniejsza ryzyko wystąpienia błędów. Wszystkie kontrakty dostępne są na GitHubie. Nas interesuje ten o nazwie ERC20. Poniżej znajdziesz implementację tokena, wykorzystującą wspomniany kontrakt.

//SPDX-License-Identifier: MIT
pragma solidity "0.8.7";
 
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
 
contract TokenV2 is ERC20 {
   constructor(
       uint256 _totalSupply,
       string memory _name,
       string memory _symbol
   ) ERC20(_name, _symbol) {
       _mint(msg.sender, _totalSupply);
   }
}

Powyższy kontrakt ma dokładnie taką samą funkcjonalność, jak ten, który stworzyliśmy wcześniej. Nie da się nie zauważyć, że kod nowego kontraktu jest zdecydowanie krótszy, a przynajmniej ta jego część, którą my musieliśmy napisać. Wszystkie niezbędne funkcje zostały zaimplementowane w pliku ERC20.sol, po którym jedynie dziedziczymy. Musimy tylko wywołać konstruktor z odpowiednimi parametrami.

Może Cię zastanawiać, czym jest funkcja “_mint”. Jest to nic innego, jak ustawienie parametru “totalSupply” oraz przypisanie całej jego wartości do portfela wygrywającego kontrakt. Wcześniej musieliśmy to zrobić ręcznie, tutaj mamy do tego gotową funkcję.

Kontrakt ERC20 dostarczony przez OpenZeppelin posiada również kilka dodatkowych zabezpieczeń. Dlatego polecam Ci zapoznać się szczegółowo z jego implementacją.

Po raz kolejny wykonaliśmy kawał dobrej roboty. Stworzyliśmy kolejny kontrakt, tym razem reprezentujący nową kryptowalutę. Co prawda jej funkcjonalność nie wyróżnia się niczym szczególnym, dlatego warto pobawić się nieco kodem. Możesz np. spróbować zaimplementować funkcjonalność, która przy każdym wykonanym transferze, pobierze pewien procent i usunie go z puli dostępnych tokenów. Możliwości jest wiele, a jedynym ograniczeniem jest Twoja wyobraźnia.

Mam nadzieję, że od tej pory już nie tylko rozumiesz czym są kryptowaluty, ale również potrafisz stworzyć swoją własną. W kolejnym wpisie ponownie poruszam temat tokenów, jednak ich zastosowanie będzie całkowicie inne od dotychczas poznanych. Następnym razem porozmawiamy o NFT :).

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