Przejdź do głównej treści

Przewodnik po kontrakcie ERC-721 w języku Vyper

Vyper
erc-721
Python
Początkujący
Ori Pomerantz
1 kwietnia 2021
18 minut czytania

Wprowadzenie

Standard ERC-721 jest używany do przechowywania własności niewymienialnych tokenów (NFT - Non-Fungible Tokens). Tokeny ERC-20 zachowują się jak towar, ponieważ nie ma różnicy między poszczególnymi tokenami. W przeciwieństwie do nich, tokeny ERC-721 są zaprojektowane dla aktywów, które są podobne, ale nie identyczne, takich jak różne kreskówkowe koty (opens in a new tab) lub akty własności różnych nieruchomości.

W tym artykule przeanalizujemy kontrakt ERC-721 autorstwa Ryuyi Nakamury (opens in a new tab). Ten kontrakt jest napisany w języku Vyper (opens in a new tab), języku kontraktów podobnym do języka Python, zaprojektowanym tak, aby trudniej było w nim napisać niebezpieczny kod niż w języku Solidity.

Kontrakt

# @dev Implementacja standardu niewymiennego tokena ERC-721.
# @author Ryuya Nakamura (@nrryuya)
# Zmodyfikowano z: https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy

Komentarze w języku Vyper, podobnie jak w języku Python, zaczynają się od znaku hash (#) i trwają do końca linii. Komentarze zawierające @<keyword> są używane przez NatSpec (opens in a new tab) do tworzenia dokumentacji czytelnej dla człowieka.

from vyper.interfaces import ERC721

implements: ERC721

Interfejs ERC-721 jest wbudowany w język Vyper. Definicję kodu możesz zobaczyć tutaj (opens in a new tab). Definicja interfejsu jest napisana w języku Python, a nie Vyper, ponieważ interfejsy są używane nie tylko wewnątrz blockchaina, ale także podczas wysyłania do blockchaina transakcji z zewnętrznego klienta, który może być napisany w języku Python.

Pierwsza linia importuje interfejs, a druga określa, że implementujemy go w tym miejscu.

Interfejs ERC721Receiver

# Interfejs dla kontraktu wywoływanego przez safeTransferFrom()
interface ERC721Receiver:
    def onERC721Received(

ERC-721 obsługuje dwa rodzaje transferów:

  • transferFrom, który pozwala nadawcy określić dowolny adres docelowy i przenosi odpowiedzialność za transfer na nadawcę. Oznacza to, że możesz wykonać transfer na nieprawidłowy adres, w którym to przypadku NFT przepada na zawsze.
  • safeTransferFrom, który sprawdza, czy adres docelowy jest kontraktem. Jeśli tak, kontrakt ERC-721 pyta kontrakt odbierający, czy chce otrzymać NFT.

Aby odpowiedzieć na żądania safeTransferFrom, kontrakt odbierający musi implementować ERC721Receiver.

            _operator: address,
            _from: address,

Adres _from to obecny właściciel tokena. Adres _operator to ten, który zażądał transferu (te dwa mogą nie być takie same z powodu limitów wydatków).

            _tokenId: uint256,

Identyfikatory tokenów ERC-721 mają 256 bitów. Zazwyczaj są one tworzone poprzez haszowanie opisu tego, co reprezentuje token.

            _data: Bytes[1024]

Żądanie może zawierać do 1024 bajtów danych użytkownika.

        ) -> bytes32: view

Aby zapobiec przypadkom, w których kontrakt przypadkowo akceptuje transfer, zwracana wartość nie jest wartością logiczną (boolean), ale 256 bitami o określonej wartości.

Ta funkcja to view, co oznacza, że może odczytywać stan blockchaina, ale nie może go modyfikować.

Zdarzenia

Zdarzenia są emitowane w celu informowania użytkowników i serwerów poza blockchainem o zdarzeniach. Należy pamiętać, że treść zdarzeń nie jest dostępna dla kontraktów na blockchainie.

Jest to podobne do zdarzenia Transfer w ERC-20, z tą różnicą, że raportujemy tokenId zamiast kwoty. Nikt nie jest właścicielem adresu zerowego, więc zgodnie z konwencją używamy go do raportowania tworzenia i niszczenia tokenów.

Zatwierdzenie (approval) w ERC-721 jest podobne do limitu wydatków (allowance) w ERC-20. Określony adres ma pozwolenie na transfer określonego tokena. Daje to kontraktom mechanizm reagowania, gdy akceptują token. Kontrakty nie mogą nasłuchiwać zdarzeń, więc jeśli po prostu prześlesz im token, nie będą o tym „wiedzieć”. W ten sposób właściciel najpierw przesyła zatwierdzenie, a następnie wysyła żądanie do kontraktu: „Zatwierdziłem dla ciebie transfer tokena X, proszę zrób to...”.

Jest to wybór projektowy, aby standard ERC-721 był podobny do standardu ERC-20. Ponieważ tokeny ERC-721 są niewymienialne, kontrakt może również zidentyfikować, że otrzymał określony token, sprawdzając jego własność.

Czasami przydatne jest posiadanie operatora, który może zarządzać wszystkimi tokenami konta określonego typu (tymi, które są zarządzane przez określony kontrakt), podobnie jak w przypadku pełnomocnictwa. Na przykład, mogę chcieć przekazać takie uprawnienia kontraktowi, który sprawdza, czy nie kontaktowałem się z nim przez sześć miesięcy, a jeśli tak, rozdziela moje aktywa moim spadkobiercom (jeśli jeden z nich o to poprosi, kontrakty nie mogą nic zrobić bez wywołania przez transakcję). W ERC-20 możemy po prostu przyznać wysoki limit wydatków kontraktowi spadkowemu, ale to nie działa w przypadku ERC-721, ponieważ tokeny nie są wymienialne. To jest odpowiednik tego rozwiązania.

Wartość approved mówi nam, czy zdarzenie dotyczy zatwierdzenia, czy wycofania zatwierdzenia.

Zmienne stanu

Te zmienne zawierają obecny stan tokenów: które z nich są dostępne i kto jest ich właścicielem. Większość z nich to obiekty HashMap, jednokierunkowe mapowania istniejące między dwoma typami (opens in a new tab).

# @dev Mapowanie z ID NFT na adres, który jest jego właścicielem.
idToOwner: HashMap[uint256, address]

# @dev Mapowanie z ID NFT na zatwierdzony adres.
idToApprovals: HashMap[uint256, address]

Tożsamości użytkowników i kontraktów w Ethereum są reprezentowane przez 160-bitowe adresy. Te dwie zmienne mapują identyfikatory tokenów na ich właścicieli i osoby zatwierdzone do ich transferu (maksymalnie jedna dla każdego). W Ethereum, niezainicjowane dane zawsze wynoszą zero, więc jeśli nie ma właściciela lub zatwierdzonego transferującego, wartość dla tego tokena wynosi zero.

# @dev Mapowanie z adresu właściciela na liczbę jego tokenów.
ownerToNFTokenCount: HashMap[address, uint256]

Ta zmienna przechowuje liczbę tokenów dla każdego właściciela. Nie ma mapowania od właścicieli do tokenów, więc jedynym sposobem na zidentyfikowanie tokenów posiadanych przez określonego właściciela jest spojrzenie wstecz w historię zdarzeń blockchaina i znalezienie odpowiednich zdarzeń Transfer. Możemy użyć tej zmiennej, aby wiedzieć, kiedy mamy wszystkie NFT i nie musimy szukać dalej w przeszłości.

Należy pamiętać, że ten algorytm działa tylko dla interfejsów użytkownika i zewnętrznych serwerów. Kod działający na samym blockchainie nie może odczytywać przeszłych zdarzeń.

# @dev Mapowanie z adresu właściciela na mapowanie adresów operatorów.
ownerToOperators: HashMap[address, HashMap[address, bool]]

Konto może mieć więcej niż jednego operatora. Proste HashMap jest niewystarczające do ich śledzenia, ponieważ każdy klucz prowadzi do pojedynczej wartości. Zamiast tego możesz użyć HashMap[address, bool] jako wartości. Domyślnie wartość dla każdego adresu to False, co oznacza, że nie jest on operatorem. W razie potrzeby możesz ustawić wartości na True.

# @dev Adres mintera, który może wybijać token
minter: address

Nowe tokeny muszą być w jakiś sposób tworzone. W tym kontrakcie istnieje tylko jeden podmiot, który ma do tego prawo, minter. Prawdopodobnie będzie to wystarczające na przykład dla gry. Do innych celów może być konieczne stworzenie bardziej skomplikowanej logiki biznesowej.

# @dev Mapowanie id interfejsu na wartość bool określającą, czy jest on obsługiwany
supportedInterfaces: HashMap[bytes32, bool]

# @dev ID interfejsu ERC-165 dla ERC-165
ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7

# @dev ID interfejsu ERC-165 dla ERC-721
ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd

ERC-165 (opens in a new tab) określa mechanizm, dzięki któremu kontrakt może ujawnić, w jaki sposób aplikacje mogą się z nim komunikować i z jakimi standardami ERC jest zgodny. W tym przypadku kontrakt jest zgodny z ERC-165 i ERC-721.

Funkcje

Są to funkcje, które faktycznie implementują ERC-721.

Konstruktor

@external
def __init__():

W języku Vyper, podobnie jak w języku Python, funkcja konstruktora nazywa się __init__.

    """
    @dev Konstruktor kontraktu.
    """

W języku Python i w języku Vyper można również utworzyć komentarz, określając wieloliniowy ciąg znaków (który zaczyna się i kończy na """) i nie używając go w żaden sposób. Te komentarze mogą również zawierać NatSpec (opens in a new tab).

    self.supportedInterfaces[ERC165_INTERFACE_ID] = True
    self.supportedInterfaces[ERC721_INTERFACE_ID] = True
    self.minter = msg.sender

Aby uzyskać dostęp do zmiennych stanu, używasz self.<variable name> (ponownie, tak samo jak w języku Python).

Funkcje widoku (View)

Są to funkcje, które nie modyfikują stanu blockchaina, a zatem mogą być wykonywane za darmo, jeśli są wywoływane zewnętrznie. Jeśli funkcje widoku są wywoływane przez kontrakt, nadal muszą być wykonane na każdym węźle, a zatem kosztują gaz.

@view
@external

Te słowa kluczowe przed definicją funkcji, które zaczynają się od znaku małpy (@), nazywane są dekoratorami. Określają one okoliczności, w jakich funkcja może zostać wywołana.

  • @view określa, że ta funkcja jest widokiem.
  • @external określa, że ta konkretna funkcja może być wywoływana przez transakcje i przez inne kontrakty.
def supportsInterface(_interfaceID: bytes32) -> bool:

W przeciwieństwie do języka Python, Vyper jest językiem statycznie typowanym (opens in a new tab). Nie można zadeklarować zmiennej ani parametru funkcji bez określenia typu danych (opens in a new tab). W tym przypadku parametrem wejściowym jest bytes32, wartość 256-bitowa (256 bitów to natywny rozmiar słowa Maszyny Wirtualnej Ethereum). Wynikiem jest wartość logiczna (boolean). Zgodnie z konwencją, nazwy parametrów funkcji zaczynają się od podkreślenia (_).

    """
    @dev Identyfikacja interfejsu jest określona w ERC-165.
    @param _interfaceID Id interfejsu
    """
    return self.supportedInterfaces[_interfaceID]

Zwraca wartość z mapy HashMap self.supportedInterfaces, która jest ustawiana w konstruktorze (__init__).

### FUNKCJE WIDOKU ###

Są to funkcje widoku, które udostępniają informacje o tokenach użytkownikom i innym kontraktom.

Ta linia zapewnia (assert) (opens in a new tab), że _owner nie jest zerem. Jeśli tak jest, występuje błąd i operacja zostaje wycofana.

W Maszynie Wirtualnej Ethereum (EVM) każda pamięć, w której nie ma zapisanej wartości, wynosi zero. Jeśli pod _tokenId nie ma tokena, to wartość self.idToOwner[_tokenId] wynosi zero. W takim przypadku funkcja zostaje wycofana.

Zauważ, że getApproved może zwrócić zero. Jeśli token jest ważny, zwraca self.idToApprovals[_tokenId]. Jeśli nie ma zatwierdzającego, wartość ta wynosi zero.

Ta funkcja sprawdza, czy _operator ma pozwolenie na zarządzanie wszystkimi tokenami _owner w tym kontrakcie. Ponieważ może być wielu operatorów, jest to dwupoziomowa mapa HashMap.

Funkcje pomocnicze transferu

Te funkcje implementują operacje, które są częścią transferu lub zarządzania tokenami.


### FUNKCJE POMOCNICZE TRANSFERU ###

@view
@internal

Ten dekorator, @internal, oznacza, że funkcja jest dostępna tylko z innych funkcji w ramach tego samego kontraktu. Zgodnie z konwencją, nazwy tych funkcji również zaczynają się od podkreślenia (_).

Istnieją trzy sposoby, w jakie adres może uzyskać pozwolenie na transfer tokena:

  1. Adres jest właścicielem tokena
  2. Adres jest zatwierdzony do wydania tego tokena
  3. Adres jest operatorem dla właściciela tokena

Powyższa funkcja może być widokiem, ponieważ nie zmienia stanu. Aby zmniejszyć koszty operacyjne, każda funkcja, która może być widokiem, powinna być widokiem.

Gdy występuje problem z transferem, wycofujemy wywołanie.

Zmieniaj wartość tylko wtedy, gdy jest to konieczne. Zmienne stanu znajdują się w pamięci (storage). Zapis do pamięci jest jedną z najdroższych operacji wykonywanych przez EVM (Maszynę Wirtualną Ethereum) (pod względem gazu). Dlatego dobrym pomysłem jest zminimalizowanie tego, nawet zapisanie istniejącej wartości wiąże się z wysokim kosztem.

Mamy tę wewnętrzną funkcję, ponieważ istnieją dwa sposoby na transfer tokenów (zwykły i bezpieczny), ale chcemy mieć tylko jedno miejsce w kodzie, w którym to robimy, aby ułatwić audyt.

Aby wyemitować zdarzenie w języku Vyper, używasz instrukcji log (zobacz tutaj, aby uzyskać więcej szczegółów (opens in a new tab)).

Funkcje transferu

Ta funkcja pozwala na transfer na dowolny adres. O ile adres nie należy do użytkownika lub kontraktu, który wie, jak transferować tokeny, każdy przetransferowany token utknie pod tym adresem i będzie bezużyteczny.

Można najpierw wykonać transfer, ponieważ jeśli wystąpi problem, i tak wycofamy operację, więc wszystko, co zostało zrobione w wywołaniu, zostanie anulowane.

    if _to.is_contract: # sprawdza, czy `_to` to adres kontraktu

Najpierw sprawdź, czy adres jest kontraktem (czy ma kod). Jeśli nie, załóż, że jest to adres użytkownika i użytkownik będzie mógł użyć tokena lub go przetransferować. Ale niech to nie uśpi twojej czujności. Możesz stracić tokeny, nawet przy użyciu safeTransferFrom, jeśli przetransferujesz je na adres, dla którego nikt nie zna klucza prywatnego.

        returnValue: bytes32 = ERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data)

Wywołaj kontrakt docelowy, aby sprawdzić, czy może on odbierać tokeny ERC-721.

        # Zgłasza wyjątek, jeśli cel transferu to kontrakt, który nie implementuje 'onERC721Received'
        assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes32)

Jeśli miejscem docelowym jest kontrakt, ale taki, który nie akceptuje tokenów ERC-721 (lub który zdecydował się nie akceptować tego konkretnego transferu), wycofaj operację.

Zgodnie z konwencją, jeśli nie chcesz mieć zatwierdzającego, wyznaczasz adres zerowy, a nie siebie.

    # Sprawdza wymagania
    senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender
    senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender]
    assert (senderIsOwner or senderIsApprovedForAll)

Aby ustawić zatwierdzenie, możesz być właścicielem lub operatorem autoryzowanym przez właściciela.

Wybijanie nowych tokenów i niszczenie istniejących

Konto, które utworzyło kontrakt, to minter, superużytkownik, który jest upoważniony do wybijania nowych NFT. Jednak nawet on nie ma prawa spalić istniejących tokenów. Może to zrobić tylko właściciel lub podmiot upoważniony przez właściciela.

### FUNKCJE WYBIJANIA I SPALANIA ###

@external
def mint(_to: address, _tokenId: uint256) -> bool:

Ta funkcja zawsze zwraca True, ponieważ jeśli operacja się nie powiedzie, zostaje wycofana.

Tylko wybijający (konto, które utworzyło kontrakt ERC-721) może wybijać nowe tokeny. Może to stanowić problem w przyszłości, jeśli będziemy chcieli zmienić tożsamość wybijającego. W kontrakcie produkcyjnym prawdopodobnie chciałbyś mieć funkcję, która pozwala wybijającemu na transfer uprawnień do wybijania na kogoś innego.

    # Zgłasza wyjątek, jeśli `_to` to adres zerowy
    assert _to != ZERO_ADDRESS
    # Dodaje NFT. Zgłasza wyjątek, jeśli `_tokenId` jest własnością kogoś
    self._addTokenTo(_to, _tokenId)
    log Transfer(ZERO_ADDRESS, _to, _tokenId)
    return True

Zgodnie z konwencją, wybijanie nowych tokenów liczy się jako transfer z adresu zerowego.

Każdy, kto ma pozwolenie na transfer tokena, ma prawo go spalić. Chociaż spalenie wydaje się równoznaczne z transferem na adres zerowy, adres zerowy w rzeczywistości nie otrzymuje tokena. Pozwala nam to zwolnić całą pamięć, która była używana dla tokena, co może zmniejszyć koszt gazu transakcji.

Korzystanie z tego kontraktu

W przeciwieństwie do języka Solidity, Vyper nie posiada dziedziczenia. Jest to celowy wybór projektowy, aby kod był jaśniejszy, a tym samym łatwiejszy do zabezpieczenia. Aby więc stworzyć własny kontrakt ERC-721 w języku Vyper, bierzesz ten kontrakt (opens in a new tab) i modyfikujesz go, aby zaimplementować pożądaną logikę biznesową.

Podsumowanie

W ramach podsumowania, oto kilka najważniejszych koncepcji w tym kontrakcie:

  • Aby odbierać tokeny ERC-721 za pomocą bezpiecznego transferu, kontrakty muszą implementować interfejs ERC721Receiver.
  • Nawet jeśli używasz bezpiecznego transferu, tokeny nadal mogą utknąć, jeśli wyślesz je na adres, którego klucz prywatny jest nieznany.
  • Gdy występuje problem z operacją, dobrym pomysłem jest revert (wycofanie) wywołania, zamiast po prostu zwracać wartość błędu.
  • Tokeny ERC-721 istnieją, gdy mają właściciela.
  • Istnieją trzy sposoby na uzyskanie autoryzacji do transferu NFT. Możesz być właścicielem, być zatwierdzonym dla określonego tokena, lub być operatorem dla wszystkich tokenów właściciela.
  • Przeszłe zdarzenia są widoczne tylko poza blockchainem. Kod działający wewnątrz blockchaina nie może ich przeglądać.

Teraz idź i implementuj bezpieczne kontrakty w języku Vyper.

Zobacz tutaj więcej moich prac (opens in a new tab).