Omówienie kontraktu ERC-721 w języku Vyper
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.vyKomentarze 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 ERC7212
3implements: ERC721Interfejs 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: viewAby 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 dowolna3# liczba NFT może zostać utworzona i przypisana bez emitowania zdarzenia Transfer. W momencie dowolnego4# 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 zerowy2# 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śli6# cofnięte).7event ApprovalForAll:8 owner: indexed(address)9 operator: indexed(address)10 approved: boolCzasami 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ć token2minter: addressNowe 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ługiwany2supportedInterfaces: HashMap[bytes32, bool]3
4# @dev ID interfejsu ERC165 dla ERC1655ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a76
7# @dev ID interfejsu ERC165 dla ERC7218ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cdERC-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@external2def __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] = True2 self.supportedInterfaces[ERC721_INTERFACE_ID] = True3 self.minter = msg.senderAby 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@view2@externalTe 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.
@viewokreśla, że ta funkcja jest widokiem.@externalokreś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 interfejsu4 """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@view2@external3def 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_ADDRESSTa 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@view4@external5def 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 NFT13 assert owner != ZERO_ADDRESS14 return ownerW 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@view2@external3def 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 NFT10 assert self.idToOwner[_tokenId] != ZERO_ADDRESS11 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@view2@external3def 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@view5@internalTen 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 tokena4 @param spender adres podmiotu wydającego do zapytania5 @param tokenId identyfikator uint256 tokena do przetransferowania6 @return bool, czy msg.sender jest zatwierdzony dla danego identyfikatora tokena,7 jest operatorem właściciela lub jest właścicielem tokena8 """9 owner: address = self.idToOwner[_tokenId]10 spenderIsOwner: bool = owner == _spender11 spenderIsApproved: bool = _spender == self.idToApprovals[_tokenId]12 spenderIsApprovedForAll: bool = (self.ownerToOperators[owner])[_spender]13 return (spenderIsOwner or spenderIsApproved) or spenderIsApprovedForAllIstnieją trzy sposoby, w jakie adres może być uprawniony do transferu tokena:
- Adres jest właścicielem tokena
- Adres jest zatwierdzony do wydania tego tokena
- 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@internal2def _addTokenTo(_to: address, _tokenId: uint256):3 """4 @dev Dodaje NFT do danego adresu5 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_ADDRESS9 # Zmień właściciela10 self.idToOwner[_tokenId] = _to11 # Zmień śledzenie licznika12 self.ownerToNFTokenCount[_to] += 113
14
15@internal16def _removeTokenFrom(_from: address, _tokenId: uint256):17 """18 @dev Usuwa NFT z danego adresu19 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ścicielem22 assert self.idToOwner[_tokenId] == _from23 # Zmień właściciela24 self.idToOwner[_tokenId] = ZERO_ADDRESS25 # Zmień śledzenie licznika26 self.ownerToNFTokenCount[_from] -= 1Gdy wystąpi problem z transferem, cofamy wywołanie.
1@internal2def _clearApproval(_owner: address, _tokenId: uint256):3 """4 @dev Czyści zatwierdzenie danego adresu5 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ścicielem8 assert self.idToOwner[_tokenId] == _owner9 if self.idToApprovals[_tokenId] != ZERO_ADDRESS:10 # Resetuj zatwierdzenia11 self.idToApprovals[_tokenId] = ZERO_ADDRESSZmieniaj 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@internal2def _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 zatwierdzonym6 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ź wymagania2 assert self._isApprovedOrOwner(_sender, _tokenId)3 # Zgłasza błąd, jeśli `_to` jest adresem zerowym4 assert _to != ZERO_ADDRESS5 # Wyczyść zatwierdzenie. Zgłasza błąd, jeśli `_from` nie jest obecnym właścicielem6 self._clearApproval(_from, _tokenId)7 # Usuń NFT. Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT8 self._removeTokenFrom(_from, _tokenId)9 # Dodaj NFT10 self._addTokenTo(_to, _tokenId)11 # Zarejestruj transfer12 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@external5def 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 zatwierdzonym8 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 razie13 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@external2def 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 lub11 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śli16 zwracana wartość nie jest `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.17 UWAGA: bytes4 jest reprezentowane przez bytes32 z dopełnieniem18 @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 kontraktuNajpierw 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@external2def 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 NFT13 assert owner != ZERO_ADDRESS14 # Zgłasza błąd, jeśli `_approved` jest obecnym właścicielem15 assert _approved != ownerZgodnie z konwencją, jeśli nie chcesz mieć podmiotu zatwierdzającego, wyznaczasz adres zerowy, a nie siebie.
1 # Sprawdź wymagania2 senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender3 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 zatwierdzenie2 self.idToApprovals[_tokenId] = _approved3 log Approval(owner, _approved, _tokenId)4
5
6@external7def setApprovalForAll(_operator: address, _approved: bool):8 """9 @dev Włącza lub wyłącza zatwierdzenie dla strony trzeciej („operatora”) do zarządzania wszystkimi10 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.sender18 self.ownerToOperators[msg.sender][_operator] = _approved19 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@external4def 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ów3 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 minterem11 assert msg.sender == self.minterTylko 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 zerowym2 assert _to != ZERO_ADDRESS3 # 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 TrueZgodnie z konwencją wybijanie nowych tokenów liczy się jako transfer z adresu zerowego.
1
2@external3def 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 zatwierdzonym7 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ź wymagania12 assert self._isApprovedOrOwner(msg.sender, _tokenId)13 owner: address = self.idToOwner[_tokenId]14 # Zgłasza błąd, jeśli `_tokenId` nie jest prawidłowym NFT15 assert owner != ZERO_ADDRESS16 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