Przejdź do głównej zawartości

Omówienie kontraktu ERC-721 w języku Vyper

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

Wprowadzenie

Standard ERC-721 służy do przechowywania własności niewymienialnych tokenów (NFT). 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ą przeznaczone dla aktywów, które są podobne, ale nie identyczne, takich jak różne kreskówki z kotami (opens in a new tab) lub tytuły własności do różnych nieruchomości.

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

Kontrakt

1# @dev Implementacja standardu niewymienialnych tokenów ERC-721.
2# @author Ryuya Nakamura (@nrryuya)
3# Zmodyfikowano na podstawie: https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy

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

1from vyper.interfaces import ERC721
2
3implements: ERC721

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

Pierwsza linia importuje interfejs, a druga określa, że go tutaj implementujemy.

Interfejs ERC721Receiver

1# Interfejs dla kontraktu wywoływanego przez safeTransferFrom()
2interface ERC721Receiver:
3 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żna dokonać transferu na nieprawidłowy adres, w takim przypadku NFT zostanie utracony 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 zaimplementować ERC721Receiver.

1 _operator: address,
2 _from: address,

Adres _from to obecny właściciel tokena. Adres _operator to ten, który zażądał transferu (te dwa adresy mogą nie być takie same ze względu na pozwolenia).

1 _tokenId: uint256,

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

1 _data: Bytes[1024]

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

1 ) -> bytes32: view

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

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

Zdarzenia

Zdarzenia (opens in a new tab) są emitowane w celu informowania użytkowników i serwerów spoza blockchaina o zdarzeniach. Należy pamiętać, że zawartość zdarzeń nie jest dostępna dla kontraktów na blockchainie.

1# @dev Emitowane, gdy własność dowolnego NFT zmienia się w wyniku dowolnego mechanizmu. To zdarzenie jest emitowane, gdy NFT są
2# tworzone (`from` == 0) i niszczone (`to` == 0). Wyjątek: podczas tworzenia kontraktu dowolna
3# liczba NFT może zostać utworzona i przypisana bez emitowania zdarzenia Transfer. W momencie dowolnego
4# transferu zatwierdzony adres dla tego NFT (jeśli istnieje) jest resetowany do zera.
5# @param _from Nadawca NFT (jeśli adres jest adresem zerowym, oznacza to utworzenie tokena).
6# @param _to Odbiorca NFT (jeśli adres jest adresem zerowym, oznacza to zniszczenie tokena).
7# @param _tokenId NFT, które zostało przetransferowane.
8event Transfer:
9 sender: indexed(address)
10 receiver: indexed(address)
11 tokenId: indexed(uint256)

Jest to podobne do zdarzenia Transfer w ERC-20, z tym wyjątkiem, że zamiast kwoty podajemy tokenId. Nikt nie jest właścicielem adresu zerowego, więc umownie używamy go do zgłaszania tworzenia i niszczenia tokenów.

1# @dev Emitowane, gdy zatwierdzony adres dla NFT jest zmieniany lub ponownie zatwierdzany. Adres zerowy
2# oznacza, że nie ma zatwierdzonego adresu. Kiedy emitowane jest zdarzenie Transfer, oznacza to również,
3# że zatwierdzony adres dla tego NFT (jeśli istnieje) jest resetowany do zera.
4# @param _owner Właściciel NFT.
5# @param _approved Adres, który zatwierdzamy.
6# @param _tokenId NFT, które zatwierdzamy.
7event Approval:
8 owner: indexed(address)
9 approved: indexed(address)
10 tokenId: indexed(uint256)

Zatwierdzenie ERC-721 jest podobne do pozwolenia (allowance) w ERC-20. Określony adres ma pozwolenie na transfer określonego tokena. Daje to mechanizm, dzięki któremu kontrakty mogą reagować na przyjęcie tokena. Kontrakty nie mogą nasłuchiwać zdarzeń, więc jeśli po prostu prześlesz im token, nie będą o tym „wiedziały”. W ten sposób właściciel najpierw przesyła zatwierdzenie, a następnie wysyła żądanie do kontraktu: „Zatwierdziłem transfer tokena X, proszę go wykonać...”.

Jest to wybór projektowy mający na celu upodobnienie standardu ERC-721 do standardu ERC-20. Ponieważ tokeny ERC-721 nie są wymienialne, kontrakt może również zidentyfikować, że otrzymał określony token, sprawdzając jego własność.

1# @dev Emitowane, gdy operator jest włączany lub wyłączany dla właściciela. Operator może zarządzać
2# wszystkimi NFT właściciela.
3# @param _owner Właściciel NFT.
4# @param _operator Adres, któremu ustawiamy uprawnienia operatora.
5# @param _approved Status uprawnień operatora (true, jeśli uprawnienia operatora są nadane, a false, jeśli
6# cofnięte).
7event ApprovalForAll:
8 owner: indexed(address)
9 operator: indexed(address)
10 approved: bool

Czasami przydatne jest posiadanie operatora, który może zarządzać wszystkimi tokenami konta określonego typu (tymi, którymi zarządza określony kontrakt), podobnie jak pełnomocnictwo. Na przykład mogę chcieć nadać takie uprawnienie kontraktowi, który sprawdza, czy nie kontaktowałem się z nim przez sześć miesięcy, a jeśli tak, to rozdziela moje aktywa między moich spadkobierców (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 dać wysokie pozwolenie kontraktowi spadkowemu, ale to nie działa w przypadku ERC-721, ponieważ tokeny nie są wymienialne. To jest odpowiednik.

Wartość approved mówi nam, czy zdarzenie dotyczy zatwierdzenia, czy cofnięcia zatwierdzenia.

Zmienne stanu

Zmienne te zawierają aktualny stan tokenów: które z nich są dostępne i kto je posiada. Większość z nich to obiekty HashMap, jednokierunkowe mapowania, które istnieją między dwoma typami (opens in a new tab).

1# @dev Mapowanie z ID NFT na adres, który jest jego właścicielem.
2idToOwner: HashMap[uint256, address]
3
4# @dev Mapowanie z ID NFT na zatwierdzony adres.
5idToApprovals: 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 do ich właścicieli i osób zatwierdzonych do ich transferu (maksymalnie jedna na każdy token). W Ethereum niezainicjowane dane są zawsze zerowe, więc jeśli nie ma właściciela lub zatwierdzonego podmiotu transferującego, wartość dla danego tokena jest zerowa.

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

Ta zmienna przechowuje liczbę tokenów dla każdego właściciela. Nie ma mapowania z właścicieli na tokeny, więc jedynym sposobem na zidentyfikowanie tokenów, które posiada dany właściciel, jest prześledzenie historii zdarzeń w blockchainie i odnalezienie odpowiednich zdarzeń Transfer. Możemy użyć tej zmiennej, aby wiedzieć, kiedy mamy wszystkie NFT i nie musimy szukać dalej w czasie.

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

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

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

1# @dev Adres mintera (podmiotu wybijającego), który może wybić token
2minter: address

Nowe tokeny muszą być w jakiś sposób tworzone. W tym kontrakcie jest jeden podmiot, który ma do tego prawo, czyli minter. To prawdopodobnie wystarczy na przykład w przypadku gry. Do innych celów może być konieczne stworzenie bardziej skomplikowanej logiki biznesowej.

1# @dev Mapowanie ID interfejsu na wartość boolowską informującą, czy jest on obsługiwany
2supportedInterfaces: HashMap[bytes32, bool]
3
4# @dev ID interfejsu ERC165 dla ERC165
5ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7
6
7# @dev ID interfejsu ERC165 dla ERC721
8ERC721_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ć, a także z którymi standardami ERC jest zgodny. W tym przypadku kontrakt jest zgodny z ERC-165 i ERC-721.

Funkcje

To są funkcje, które faktycznie implementują ERC-721.

Konstruktor

1@external
2def __init__():

W Vyper, podobnie jak w Pythonie, funkcja konstruktora nazywa się __init__.

1 """
2 @dev Konstruktor kontraktu.
3 """

W Pythonie i w 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).

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

Aby uzyskać dostęp do zmiennych stanu, użyj self.<nazwa zmiennej> (ponownie, tak samo jak w Pythonie).

Funkcje widoku

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

1@view
2@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 których funkcja może być 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.
1def supportsInterface(_interfaceID: bytes32) -> bool:

W przeciwieństwie do Pythona Vyper jest językiem z typowaniem statycznym (opens in a new tab). Nie można zadeklarować zmiennej ani parametru funkcji bez zidentyfikowania 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 Wirtualnej Maszyny Ethereum). Wyjściem jest wartość logiczna (boolean). Zgodnie z konwencją nazwy parametrów funkcji zaczynają się od podkreślenia (_).

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

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

1### FUNKCJE WIDOKU ###
2

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

1@view
2@external
3def balanceOf(_owner: address) -> uint256:
4 """
5 @dev Zwraca liczbę NFT posiadanych przez `_owner`.
6 Zgłasza błąd, jeśli `_owner` jest adresem zerowym. NFT przypisane do adresu zerowego są uważane za nieprawidłowe.
7 @param _owner Adres, dla którego należy sprawdzić saldo.
8 """
9 assert _owner != ZERO_ADDRESS

Ta linia stwierdza (opens in a new tab), że _owner nie jest zerowy. Jeśli tak jest, występuje błąd, a operacja jest cofana.

1 return self.ownerToNFTokenCount[_owner]
2
3@view
4@external
5def ownerOf(_tokenId: uint256) -> address:
6 """
7 @dev Zwraca adres właściciela NFT.
8 Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT.
9 @param _tokenId Identyfikator NFT.
10 """
11 owner: address = self.idToOwner[_tokenId]
12 # Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT
13 assert owner != ZERO_ADDRESS
14 return owner

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

1@view
2@external
3def getApproved(_tokenId: uint256) -> address:
4 """
5 @dev Pobiera zatwierdzony adres dla pojedynczego NFT.
6 Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT.
7 @param _tokenId ID NFT, którego zatwierdzenie ma być sprawdzone.
8 """
9 # Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT
10 assert self.idToOwner[_tokenId] != ZERO_ADDRESS
11 return self.idToApprovals[_tokenId]

Należy pamiętać, że getApproved może zwrócić zero. Jeśli token jest prawidłowy, zwraca self.idToApprovals[_tokenId]. Jeśli nie ma podmiotu zatwierdzającego, ta wartość wynosi zero.

1@view
2@external
3def isApprovedForAll(_owner: address, _operator: address) -> bool:
4 """
5 @dev Sprawdza, czy `_operator` jest zatwierdzonym operatorem dla `_owner`.
6 @param _owner Adres, który jest właścicielem NFT.
7 @param _operator Adres, który działa w imieniu właściciela.
8 """
9 return (self.ownerToOperators[_owner])[_operator]

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

Funkcje pomocnicze transferu

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

1
2### FUNKCJE POMOCNICZE TRANSFERU ###
3
4@view
5@internal

Ten dekorator, @internal, oznacza, że funkcja jest dostępna tylko z innych funkcji w tym samym kontrakcie. Zgodnie z konwencją te nazwy funkcji również zaczynają się od podkreślenia (_).

1def _isApprovedOrOwner(_spender: address, _tokenId: uint256) -> bool:
2 """
3 @dev Zwraca informację, czy dany podmiot wydający może przetransferować dany identyfikator tokena
4 @param spender adres podmiotu wydającego do zapytania
5 @param tokenId identyfikator uint256 tokena do przetransferowania
6 @return bool, czy msg.sender jest zatwierdzony dla danego identyfikatora tokena,
7 jest operatorem właściciela lub jest właścicielem tokena
8 """
9 owner: address = self.idToOwner[_tokenId]
10 spenderIsOwner: bool = owner == _spender
11 spenderIsApproved: bool = _spender == self.idToApprovals[_tokenId]
12 spenderIsApprovedForAll: bool = (self.ownerToOperators[owner])[_spender]
13 return (spenderIsOwner or spenderIsApproved) or spenderIsApprovedForAll

Istnieją trzy sposoby, w jakie adres może być uprawniony do transferu 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 obniżyć koszty operacyjne, każda funkcja, która może być widokiem, powinna być widokiem.

1@internal
2def _addTokenTo(_to: address, _tokenId: uint256):
3 """
4 @dev Dodaje NFT do danego adresu
5 Zgłasza błąd, jeśli `_tokenId` jest własnością kogoś.
6 """
7 # Zgłasza błąd, jeśli `_tokenId` jest własnością kogoś
8 assert self.idToOwner[_tokenId] == ZERO_ADDRESS
9 # Zmień właściciela
10 self.idToOwner[_tokenId] = _to
11 # Zmień śledzenie licznika
12 self.ownerToNFTokenCount[_to] += 1
13
14
15@internal
16def _removeTokenFrom(_from: address, _tokenId: uint256):
17 """
18 @dev Usuwa NFT z danego adresu
19 Zgłasza błąd, jeśli `_from` nie jest obecnym właścicielem.
20 """
21 # Zgłasza błąd, jeśli `_from` nie jest obecnym właścicielem
22 assert self.idToOwner[_tokenId] == _from
23 # Zmień właściciela
24 self.idToOwner[_tokenId] = ZERO_ADDRESS
25 # Zmień śledzenie licznika
26 self.ownerToNFTokenCount[_from] -= 1

Gdy wystąpi problem z transferem, cofamy wywołanie.

1@internal
2def _clearApproval(_owner: address, _tokenId: uint256):
3 """
4 @dev Czyści zatwierdzenie danego adresu
5 Zgłasza błąd, jeśli `_owner` nie jest obecnym właścicielem.
6 """
7 # Zgłasza błąd, jeśli `_owner` nie jest obecnym właścicielem
8 assert self.idToOwner[_tokenId] == _owner
9 if self.idToApprovals[_tokenId] != ZERO_ADDRESS:
10 # Resetuj zatwierdzenia
11 self.idToApprovals[_tokenId] = ZERO_ADDRESS

Zmieniaj wartość tylko w razie potrzeby. Zmienne stanu znajdują się w pamięci (storage). Zapis do pamięci jest jedną z najdroższych operacji, jakie wykonuje EVM (Wirtualna Maszyna Ethereum) (pod względem gazu). Dlatego dobrym pomysłem jest jego minimalizowanie, nawet zapisywanie istniejącej wartości ma wysoki koszt.

1@internal
2def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: address):
3 """
4 @dev Wykonuje transfer NFT.
5 Zgłasza błąd, chyba że `msg.sender` jest obecnym właścicielem, autoryzowanym operatorem lub zatwierdzonym
6 adresem dla tego NFT. (UWAGA: `msg.sender` nie jest dozwolony w funkcji prywatnej, więc przekaż `_sender`.)
7 Zgłasza błąd, jeśli `_to` jest adresem zerowym.
8 Zgłasza błąd, jeśli `_from` nie jest obecnym właścicielem.
9 Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT.
10 """

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

1 # Sprawdź wymagania
2 assert self._isApprovedOrOwner(_sender, _tokenId)
3 # Zgłasza błąd, jeśli `_to` jest adresem zerowym
4 assert _to != ZERO_ADDRESS
5 # Wyczyść zatwierdzenie. Zgłasza błąd, jeśli `_from` nie jest obecnym właścicielem
6 self._clearApproval(_from, _tokenId)
7 # Usuń NFT. Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT
8 self._removeTokenFrom(_from, _tokenId)
9 # Dodaj NFT
10 self._addTokenTo(_to, _tokenId)
11 # Zarejestruj transfer
12 log Transfer(_from, _to, _tokenId)

Aby wyemitować zdarzenie w Vyper, użyj instrukcji log (więcej szczegółów tutaj (opens in a new tab)).

Funkcje transferu

1
2### FUNKCJE TRANSFERU ###
3
4@external
5def transferFrom(_from: address, _to: address, _tokenId: uint256):
6 """
7 @dev Zgłasza błąd, chyba że `msg.sender` jest obecnym właścicielem, autoryzowanym operatorem lub zatwierdzonym
8 adresem dla tego NFT.
9 Zgłasza błąd, jeśli `_from` nie jest obecnym właścicielem.
10 Zgłasza błąd, jeśli `_to` jest adresem zerowym.
11 Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT.
12 @notice Wywołujący jest odpowiedzialny za potwierdzenie, że `_to` jest w stanie odbierać NFT, w przeciwnym razie
13 mogą one zostać trwale utracone.
14 @param _from Obecny właściciel NFT.
15 @param _to Nowy właściciel.
16 @param _tokenId NFT do przetransferowania.
17 """
18 self._transferFrom(_from, _to, _tokenId, msg.sender)

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

1@external
2def safeTransferFrom(
3 _from: address,
4 _to: address,
5 _tokenId: uint256,
6 _data: Bytes[1024]=b""
7 ):
8 """
9 @dev Przenosi własność NFT z jednego adresu na inny.
10 Zgłasza błąd, chyba że `msg.sender` jest obecnym właścicielem, autoryzowanym operatorem lub
11 zatwierdzonym adresem dla tego NFT.
12 Zgłasza błąd, jeśli `_from` nie jest obecnym właścicielem.
13 Zgłasza błąd, jeśli `_to` jest adresem zerowym.
14 Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT.
15 Jeśli `_to` jest inteligentnym kontraktem, wywołuje `onERC721Received` na `_to` i zgłasza błąd, jeśli
16 zwracana wartość nie jest `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
17 UWAGA: bytes4 jest reprezentowane przez bytes32 z dopełnieniem
18 @param _from Obecny właściciel NFT.
19 @param _to Nowy właściciel.
20 @param _tokenId NFT do przetransferowania.
21 @param _data Dodatkowe dane bez określonego formatu, wysyłane w wywołaniu do `_to`.
22 """
23 self._transferFrom(_from, _to, _tokenId, msg.sender)

Można najpierw wykonać transfer, ponieważ w razie problemu i tak cofniemy całą operację, więc wszystko, co zostało zrobione w wywołaniu, zostanie anulowane.

1 if _to.is_contract: # sprawdź, czy `_to` jest adresem kontraktu

Najpierw sprawdź, czy adres jest kontraktem (czy ma kod). Jeśli nie, załóż, że jest to adres użytkownika, a 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 używając safeTransferFrom, jeśli przetransferujesz je na adres, do którego nikt nie zna klucza prywatnego.

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

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

1 # Zgłasza błąd, jeśli miejscem docelowym transferu jest kontrakt, który nie implementuje „onERC721Received”
2 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), operacja zostanie cofnięta.

1@external
2def approve(_approved: address, _tokenId: uint256):
3 """
4 @dev Ustawia lub ponownie zatwierdza zatwierdzony adres dla NFT. Adres zerowy wskazuje, że nie ma zatwierdzonego adresu.
5 Zgłasza błąd, chyba że `msg.sender` jest obecnym właścicielem NFT lub autoryzowanym operatorem obecnego właściciela.
6 Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT. (UWAGA: nie jest to zapisane w EIP)
7 Zgłasza błąd, jeśli `_approved` jest obecnym właścicielem. (UWAGA: nie jest to zapisane w EIP)
8 @param _approved Adres do zatwierdzenia dla danego ID NFT.
9 @param _tokenId ID tokena do zatwierdzenia.
10 """
11 owner: address = self.idToOwner[_tokenId]
12 # Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT
13 assert owner != ZERO_ADDRESS
14 # Zgłasza błąd, jeśli `_approved` jest obecnym właścicielem
15 assert _approved != owner

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

1 # Sprawdź wymagania
2 senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender
3 senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender]
4 assert (senderIsOwner or senderIsApprovedForAll)

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

1 # Ustaw zatwierdzenie
2 self.idToApprovals[_tokenId] = _approved
3 log Approval(owner, _approved, _tokenId)
4
5
6@external
7def setApprovalForAll(_operator: address, _approved: bool):
8 """
9 @dev Włącza lub wyłącza zatwierdzenie dla strony trzeciej („operatora”) do zarządzania wszystkimi
10 aktywami `msg.sender`. Emituje również zdarzenie ApprovalForAll.
11 Zgłasza błąd, jeśli `_operator` to `msg.sender`. (UWAGA: nie jest to zapisane w EIP)
12 @notice Działa to nawet wtedy, gdy nadawca nie posiada w danym momencie żadnych tokenów.
13 @param _operator Adres do dodania do zestawu autoryzowanych operatorów.
14 @param _approved True, jeśli operator jest zatwierdzony, false, aby cofnąć zatwierdzenie.
15 """
16 # Zgłasza błąd, jeśli `_operator` to `msg.sender`
17 assert _operator != msg.sender
18 self.ownerToOperators[msg.sender][_operator] = _approved
19 log ApprovalForAll(msg.sender, _operator, _approved)

Wybijanie nowych tokenów i niszczenie istniejących

Konto, które utworzyło kontrakt, jest minterem, superużytkownikiem upoważnionym do wybijania nowych NFT. Jednak nawet on nie może palić istniejących tokenów. Może to zrobić tylko właściciel lub podmiot upoważniony przez właściciela.

1### FUNKCJE WYBIJANIA I PALENIA ###
2
3@external
4def mint(_to: address, _tokenId: uint256) -> bool:

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

1 """
2 @dev Funkcja do wybijania tokenów
3 Zgłasza błąd, jeśli `msg.sender` nie jest minterem.
4 Zgłasza błąd, jeśli `_to` jest adresem zerowym.
5 Zgłasza błąd, jeśli `_tokenId` jest własnością kogoś.
6 @param _to Adres, który otrzyma wybite tokeny.
7 @param _tokenId Identyfikator tokena do wybicia.
8 @return Wartość logiczna wskazująca, czy operacja zakończyła się powodzeniem.
9 """
10 # Zgłasza błąd, jeśli `msg.sender` nie jest minterem
11 assert msg.sender == self.minter

Tylko minter (konto, które utworzyło kontrakt ERC-721) może wybijać nowe tokeny. Może to być problemem w przyszłości, jeśli będziemy chcieli zmienić tożsamość mintera. W kontrakcie produkcyjnym prawdopodobnie chciałbyś mieć funkcję, która pozwala minterowi na przekazanie uprawnień mintera komuś innemu.

1 # Zgłasza błąd, jeśli `_to` jest adresem zerowym
2 assert _to != ZERO_ADDRESS
3 # Dodaj NFT. Zgłasza błąd, jeśli `_tokenId` jest własnością kogoś
4 self._addTokenTo(_to, _tokenId)
5 log Transfer(ZERO_ADDRESS, _to, _tokenId)
6 return True

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

1
2@external
3def burn(_tokenId: uint256):
4 """
5 @dev Pali określony token ERC721.
6 Zgłasza błąd, chyba że `msg.sender` jest obecnym właścicielem, autoryzowanym operatorem lub zatwierdzonym
7 adresem dla tego NFT.
8 Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT.
9 @param _tokenId uint256 id tokena ERC721 do spalenia.
10 """
11 # Sprawdź wymagania
12 assert self._isApprovedOrOwner(msg.sender, _tokenId)
13 owner: address = self.idToOwner[_tokenId]
14 # Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT
15 assert owner != ZERO_ADDRESS
16 self._clearApproval(owner, _tokenId)
17 self._removeTokenFrom(owner, _tokenId)
18 log Transfer(owner, ZERO_ADDRESS, _tokenId)

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

Korzystanie z tego kontraktu

W przeciwieństwie do Solidity, Vyper nie ma dziedziczenia. Jest to celowy wybór projektowy, aby kod był jaśniejszy, a tym samym łatwiejszy do zabezpieczenia. Tak więc, aby stworzyć własny kontrakt ERC-721 w Vyper, bierzesz ten kontrakt i modyfikujesz go w celu zaimplementowania pożądanej logiki biznesowej.

Wnioski

Dla przypomnienia, oto niektóre z najważniejszych pomysłów w tym kontrakcie:

  • Aby odbierać tokeny ERC-721 za pomocą bezpiecznego transferu, kontrakty muszą implementować interfejs ERC721Receiver.
  • Nawet jeśli użyjesz bezpiecznego transferu, tokeny mogą utknąć, jeśli wyślesz je na adres, którego klucz prywatny jest nieznany.
  • Gdy wystąpi problem z operacją, dobrym pomysłem jest cofnięcie (revert) wywołania, a nie tylko zwrócenie wartości błędu.
  • Tokeny ERC-721 istnieją, gdy mają właściciela.
  • Istnieją trzy sposoby autoryzacji do transferu NFT. Możesz być właścicielem, być zatwierdzonym dla konkretnego 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 zobaczyć.

Teraz idź i zaimplementuj bezpieczne kontrakty Vyper.

Zobacz więcej mojej pracy tutaj (opens in a new tab).

Strona ostatnio zaktualizowana: 3 marca 2026

Czy ten samouczek był pomocny?