Jak zmienić niezmienialne – czyli o aktualizacji smart kontraktów

Przemek/ 5 marca, 2023

Blockchain jest niezmienny. Blockchain jest przejrzysty. Wszystko, co dzieje się na blockchainie można w łatwy sposób sprawdzić i zweryfikować. Raz wgrany kod na blockchain pozostanie tam na zawsze i to w niezmienionej formie. To tylko kilka z określeń, które powtarzane są jak mantra w stosunku do zdecentralizowanych technologii. Czy są one w stu procentach prawdziwe? Co w przypadku, gdy smart kontrakt wymaga usprawnień lub naprawy błędów? Na te i inne pytania postaram się odpowiedzieć w dzisiejszym wpisie.

I have a dream

Zanim przejdę do tematu aktualizacji smart kontraktów, przyjrzyjmy się krótkiej historii pewnego oprogramowania.

Piątek wieczór, wyjście ze znajomymi na miasto. Po kilkudziesięciu minutach gwarnych rozmów i kilku głębszych kuflach piwa, wpadasz na cudowny pomysł nowego oprogramowania do śledzenia aktualnego nawodnienia domowych roślin. Wszyscy Ci przyklaskują i z tą wspaniałą myślą wracasz do domu.

Kolejnego dnia z samego rana bierzesz się do poszukiwania firmy IT, która stworzy dla Ciebie aplikację. Po kilkunastu godzinach poszukiwań i równie wielu telefonach kandydat został wybrany.

Po umówieniu spotkania udajesz się do wyznaczonego biura, gdzie przedstawiasz swoją wizję, a najlepsi marketingowcy robią wszystko, byle ich firma została wybrana do tego projektu. Po dogadaniu wszystkich szczegółów i podpisaniu kontraktu zostaje przedstawiony Ci zespół najlepszych specjalistów, którzy od dziś będą do Twoich usług. Tutaj zaczyna się praca nad tworzeniem oprogramowania. Poczynając od analizy wymagań, poprzez projektowanie każdego detalu, implementację oraz testowanie, z każdym dniem zbliża się data premiery aplikacji. 

Wreszcie po wielu miesiącach ciężkiej pracy zostaje wydana pierwsza wersja, z której od teraz mogą korzystać rzesze użytkowników. Wszystko się powiodło, jednak Ty masz już pomysły na kolejne usprawnienia i nowatorskie rozwiązania, dlatego siadasz ponownie z zespołem i planujecie kolejny etap rozwoju, który jak poprzednio będzie przechodził przez wszystkie, wcześniej wspomniany fazy cyklu życia oprogramowania. Część obecnych funkcjonalności będzie musiała zostać zmieniona, część zostanie dodana, a jeszcze inne wylecą z nowej wersji. Standard w świecie IT.

Kiedy z pełnym entuzjazmem Twój zespół pracuje nad usprawnieniami, a użytkownicy z wielką radością sprawdzają nawodnienie swoich roślinek, pojawia się ON, wielki, groźny i obrzydliwy błąd (ang. bug)! Jest na tyle poważny, że wymaga szybkiej interwencji. W przeciwnym razie możesz stracić wszystkich użytkowników. Na szczęście Twój zespół to sami profesjonaliści, więc w bardzo krótkim czasie łatają dziurę. Teraz już oprogramowanie jest perfekcyjne, a Ty możesz odetchnąć z ulgą, myśląc o dalszym jego rozwoju.

W bardzo dużym przerysowaniu i uproszczeniu można powiedzieć, że tak właśnie wygląda proces tworzenia standardowego oprogramowania. Zarówno błędy jak i kolejne usprawnienia to coś, co pojawia się regularnie, dlatego każdy twórca musi być na to przygotowany. W przypadku wytwarzania oprogramowania na tradycyjne “platformy”, gdzie całość kodu oraz zarządzanie nim, jak i infrastrukturą jest scentralizowane i leży w gestii twórców, nie stanowi to większego problemu. W każdym momencie możemy coś zmienić lub usunąć, a w razie poważnych problemów nawet zatrzymać aplikację. Co jednak w przypadku smart kontraktów, które opierają się o technologię zdecentralizowaną, jaką jest blockchain?

Przecież na blockchainie nie da się nic zmienić ?!

Jedną z pierwszych rzeczy, jaką się słyszy, kiedy ktoś wspomina blockchain, jest jego tzw. niezmienność (ang. immutability). Innymi słowy można powiedzieć, że jeśli coś trafia na blockchain, to zostanie już tam na zawsze i to w dokładnie tej samej postaci. Dzięki temu blockchain traktowany jest jako technologia, która zapewnia 100% przejrzystości i zaufania. I jest to prawda, chociaż czasami źle interpretowana.

Na początku należy podkreślić, że faktycznie jeśli coś trafi na blockchain to informacja o tym zostanie tam na zawsze. Zakładając oczywiście, że mówimy o takich rozwiązaniach jak Bitcoin czy Ethereum, czyli innymi słowy o dojrzałych technologiach. Jednak warto zrozumieć, że to co jest obecnym stanem blockchaina może, a wręcz ciągle ulega zmianie. Gdyby takie coś nie było możliwe, to nie dałoby się przesłać żadnych środków między portfelami ani wykonać żadnej operacji na smart kontraktach, ponieważ nic nie mogłoby się zmienić. Dlatego w pewnym uproszczeniu można powiedzieć, że obecny stan blockchaina jest zmienny. a jedynie historyczne dane pozostają nienaruszalne.

Jak natomiast wygląda sprawa w kontekście samych smart kontraktów? Tutaj również obecny stan, czyli to co zapisane jest w kontrakcie może ulegać zmianie. Jest to analogiczna sytuacja do bazy danych, w której następuje aktualizacja przechowywanych informacji. Oczywiście cała historia transakcji związanych z konkretnym kontraktem jest przechowywana na blockchainie i możemy w stosunkowo łatwy sposób sprawdzić, jak wyglądał stan w przeszłości. Jednak jest jedna rzecz, która w tym przypadku nigdy się nie zmienia, a mianowicie sam kod smart kontraktu. Cokolwiek byśmy nie próbowali zrobić, raz wgrany kod pozostanie taki sam. Jak w takim razie poradzić sobie w sytuacji, kiedy nasze oprogramowanie wymaga aktualizacji, czy to ze względu na potrzebę zmian istniejących funkcjonalności, dodania nowych, czy, co gorsza, wykrycia poważnego błędu? 

Na szczęście jest przynajmniej kilka metod, które pomagają to zrobić nawet w takim środowisku, jakim jest blockchain. Dzisiaj omówię dwie najprostsze, a mianowicie parametryzację oraz migrację. W kolejnych wpisach wrócę do tematu i przedstawię rozwiązania zwane eternal storage, proxy oraz tzw. diamond standard. Dwie ostatnie metody wymagać będą poznania jeszcze kilku “smaczków” Solidity, dlatego w międzyczasie pojawią się również wpisy na ten temat.

Sparametryzujmy świat!

Zajmijmy się teraz wytłumaczeniem pierwszego sposobu, który pomaga w pewien sposób aktualizować to, jak zachowuje się smart kontrakt. Mowa tutaj o parametryzacji. Co prawda nie jest to do końca metoda, która pozwala na prawdziwą zmianę zachowania smart kontraktu i w większości przypadków nie będzie miała sensownego zastosowania. Jednak ze względu na jej prostotę warto o niej wspomnieć. Należy też pamiętać, że przy jej pomocy nie będziemy w stanie zmienić logiki, z jakiej korzysta nasz smart kontrakt, ani również nie możemy uaktualnić tzw. storage’u, czyli innymi słowy, nie będziemy mogli dodać ani usunąć pól w smart kontrakcie.

Zatem na czym polega najprostsza z metod? Jak nazwa może sugerować, chodzi o dodanie parametrów w każde możliwe miejsce, a następnie aktualizując ich wartość, możemy wpływać na inne rzeczy. Do lepszego zrozumienia tematu posłużę się przykładzie loterii. Poniżej znajduje się uproszczona wersja takiego kontraktu.

//SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol";

contract Lottery is Ownable {

   uint256 private multiplier;

   constructor() {
       multiplier = 1;
   }

   function reward() external view returns(uint256) {
       return 1 * multiplier;
   }

   function setMultiplier(uint256 _multiplier) external onlyOwner {
       multiplier = _multiplier;
   }
   //rest of the logic
}

Kontrakt przechowuje pole o nazwie “multiplier”, które jest właśnie daną, służącą do sparametryzowania zachowania, a konkretnie wysokości nagrody. Jeżeli twórca uznałby, że wygraną należy zwiększyć, to wystarczy zmienić wartość “multiplier” i tym sposobem zmieni się niejako zachowanie kontraktu. De facto logika pozostaje ta sama, ale otrzymywane rezultaty będą inne.

Jak widzisz nie ma w tej metodzie nic wyrafinowanego. Musisz jedynie na początku przemyśleć, co ma podlegać sterowaniu i następnie jto sparametryzować. Niestety ten sposób nie przyda się w przypadku wystąpienia błędu lub chęci całkowitej zmiany zasad gry, co jest jego ogromną wadą. Dlatego nie zatrzymujmy się i przejdźmy do czegoś bardziej elastycznego.

Migracja

Rysunek 4. Migranci
(źródło: https://www.britannica.com/topic/human-migration)

Pierwszą z metod, która daje możliwość całkowicie zmienić funkcjonalność smart kontraktu jest tzw. migracja. Zasada jaka się za nią kryje jest bardzo prosta i niektórzy twierdzą, że jest to jedyny prawdziwy sposób aktualizowania kontraktów, który jest w stu procentach zgodny z ideą niezmienności, chociaż ma również swoje wady.

Tak samo jak różnego rodzaju migranci “zabierają swoje zabawki” z jednego miejsca i rozpoczynają życie w zupełnie innym, tak samo metoda migracji kontraktów polega na stworzeniu nowego kodu, wgraniu go na blockchain i przetransportowania do nowego miejsca wszystkich poprzednich danych. Żeby lepiej zrozumieć ten zabieg, prześledźmy przykład aktualizacji tokena. Na początku stwórzmy pierwszą wersję kryptowaluty.

//SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol";

contract TheBestToken is ERC20 {

   constructor() ERC20("TheBest", "TBT") {
       _mint(msg.sender, 1000 * 1e18);
   }
}

Powyżej znajduje się kod, reprezentujący prosty token ERC20. Cała funkcjonalność oparta jest o rozwiązanie dostarczone przez OpenZeppelin, a w konstruktorze jedynie inicjalizujemy ilość tokenów. Jeżeli ERC20 jest Tobie obce, to tutaj znajdziesz szczegółowe wyjaśnienie.

Po jakimś czasie stwierdzamy jednak, że dobrze gdyby nasz token miał funkcjonalność “spalania”. Niestety nie jesteśmy w stanie zmodyfikować wcześniej stworzonego kodu, dlatego decydujemy się na metodę migracji i na samym początku tworzymy zaktualizowany kontrakt.

//SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol";

contract TheBestTokenV2 is ERC20 {

     constructor() ERC20("TheBestV2", "NTBT") {
       _mint(msg.sender, 1000 * 1e18);
   }

   function burn() external {
       // some logic
   }
}

Nowo utworzony kod wgrywamy na blockchain. Czy już wszystko gotowe? Oczywiście, że nie, ponieważ zostaje jeszcze bardzo istotna sprawa, czyli migracja danych. Przecież chcielibyśmy, aby każdy z naszych użytkowników miał dokładnie tyle nowych tokenów, ile posiadał starych, dlatego przygotowujemy kolejny kontrakt, który będzie pełnił rolę migratora. Mógłby on wyglądać np. tak:

//SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol";

contract Migrator {

   mapping(address => bool) migrated;

   IERC20 oldToken;
   IERC20 newToken;

   constructor(IERC20 _oldToken, IERC20 _newToken) {
       oldToken = _oldToken;
       newToken = _newToken;
   }

   //Old tokens need to be approved before migration
   function migrate() external {
       require(!migrated[msg.sender], "Already migrated");
       migrated[msg.sender] = true;

       uint256 balance = oldToken.balanceOf(msg.sender);
       oldToken.transferFrom(msg.sender, address(this), balance);
       newToken.transfer(msg.sender, balance);
   }
}

Stworzony kontrakt posiada referencję zarówno do nowego jak i starego tokena oraz mapowanie przechowujące informacje o już zmigrowanych użytkownikach, a jego główna funkcjonalność zamknięta jest w funkcji “migrate”. Omówmy w takim razie co się dzieje w tych kilku linijkach.

Na początku sprawdzamy, czy użytkownik nie dokonał już migracji. Przecież nie chcielibyśmy, aby mógł otrzymać więcej tokenów niż mu się należy. Jeśli jest to pierwsza próba, to zapisujemy o tym informację, sprawdzamy ile starych tokenów posiada, zabieramy mu je i przekazujemy nowe. To by było na tyle:).

Z detali technicznych musielibyśmy oczywiście najpierw zasilić kontrakt nową wersją kryptowaluty, a sam użytkownik musiałbym przed wywołaniem metody migracji wykonać operację “approve” na starym kontrakcie, ale są w kontekście pojęcia migracji nie jest to teraz istotny aspekt.

Jak widzisz przedstawiony sposób nie wymaga żadnej tajemnej wiedzy i posiada jedynie trzy kroki: stworzenie nowej wersji, przygotowanie migratora oraz wykonanie migracji przez użytkowników. Czasem da się pominąć ostatni krok i przepisać dane samemu, jednak w przypadku tokenów dobrze, żeby użytkownicy się ich pozbyli zanim otrzymają nowe. Gdyby natomiast kontrakt przechowywał jakieś inne dane, to równie dobrze my moglibyśmy je wgrać do nowego kontraktu.

Niestety mimo swej technicznej prostoty, metoda ta ma pewne uciążliwe minusy. Po pierwsze musimy przekonać użytkowników do przeniesienia się na nowe rozwiązanie. Ponieważ wszystko odbywa się na blockchainie, to nasz stary kontrakt nie zniknie w magiczny sposób i nic nie zablokuje możliwości dalszego korzystania z niego. Chyba, że przewidzieliśmy taką możliwość podczas jego tworzenia i zaimplementowaliśmy takie funkcje. W przeciwnym wypadku może się okazać, że nasi użytkownicy nie zaakceptują zmian i nie zechcą nowego tokena. Czy miałoby to sens z ich perspektywy, to już inna sprawa.

Również w sytuacji, gdy nie mamy do czynienia z krypto, przekonanie użytkowników jest kluczowym aspektem, a może nawet i ważniejszym. Należy pamiętać, że my możemy na swojej aplikacji webowej pozmieniać wszystko i zintegrować się z nowym kontraktem, przez co możemy pomyśleć, że nasi użytkownicy będą zmuszeni z niego korzystać. Jednak prawda jest taka, że do smart kontraktów można się odwołać z dowolnego miejsca, które się z nimi połączy, więc w najgorszym razie ktoś może stworzyć nową aplikację webową i pozwolić użytkownikom korzystać ze starej wersji kontraktów.

Załóżmy jednak, że wszyscy chcieliby przejść na nową wersję. I tutaj najważniejsze jest poinformowanie każdego użytkownika o zmianach. Jeżeli z naszego rozwiązania korzysta stosunkowo mała grupa osób lub wszystko odbywa się przez jeden centralny punkt w Internecie, to nie powinno być z tym problemu. Gorzej jeśli z naszego rozwiązania korzysta wiele różnych protokołów. Wtedy każdy z nich powinien dokonać takiej zmiany, ponieważ w przeciwnym razie część osób będzie wykorzystywać nowe rozwiązanie, a część stare.

Podsumowując, metoda migracji jest stosunkowo łatwa do zaimplementowania od strony technicznej. Dodatkowo sprawia, że nowy audyt bezpieczeństwa w miarę prosty, ponieważ wszystko znajduje się na świeżo stworzonym kontrakcie. Problem natomiast pojawić się może podczas samego procesu przenoszenia użytkowników i podpinania różnych rozwiązań pod nową wersję, ponieważ każdy zainteresowany musi zacząć używać nowego adresu kontraktu.

Jeżeli nie satysfakcjonują Cię dzisiaj opisane metod aktualizacji smart kontraktów i oczekujesz czegoś bardziej elastycznego to wypatruj kolejnych wpisów. Następnym razem skupimy się na tzw. eternal storage, który może być potraktowany pod pewnymi kątami jako ulepszenie metody migracji.

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