Vai al contenuto principale

Guida passo passo al contratto ERC-721 in Vyper

Vyper
erc-721
Python
Principiante
Ori Pomerantz
1 aprile 2021
20 minuti di lettura

Introduzione

Lo standard ERC-721 è utilizzato per detenere la proprietà dei token non fungibili (NFT). I token ERC-20 si comportano come una merce, perché non c'è differenza tra i singoli token. Al contrario, i token ERC-721 sono progettati per asset simili ma non identici, come diversi gatti dei cartoni animati (opens in a new tab) o titoli di diverse proprietà immobiliari.

In questo articolo analizzeremo il contratto ERC-721 di Ryuya Nakamura (opens in a new tab). Questo contratto è scritto in Vyper (opens in a new tab), un linguaggio per contratti simile a Python progettato per rendere più difficile scrivere codice insicuro rispetto a Solidity.

Il contratto

1# @dev Implementazione dello standard dei token non fungibili ERC-721.
2# @author Ryuya Nakamura (@nrryuya)
3# Modificato da: https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy

I commenti in Vyper, come in Python, iniziano con un cancelletto (#) e continuano fino alla fine della riga. I commenti che includono @<keyword> sono utilizzati da NatSpec (opens in a new tab) per produrre documentazione leggibile dall'uomo.

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

L'interfaccia ERC-721 è integrata nel linguaggio Vyper. Puoi vedere la definizione del codice qui (opens in a new tab). La definizione dell'interfaccia è scritta in Python, anziché in Vyper, perché le interfacce sono utilizzate non solo all'interno della blockchain, ma anche quando si invia alla blockchain una transazione da un client esterno, che potrebbe essere scritto in Python.

La prima riga importa l'interfaccia e la seconda specifica che la stiamo implementando qui.

L'interfaccia ERC721Receiver

1# Interfaccia per il contratto chiamato da safeTransferFrom()
2interface ERC721Receiver:
3 def onERC721Received(

L'ERC-721 supporta due tipi di trasferimento:

  • transferFrom, che consente al mittente di specificare qualsiasi indirizzo di destinazione e pone la responsabilità del trasferimento sul mittente. Ciò significa che puoi trasferire a un indirizzo non valido, nel qual caso l'NFT è perso per sempre.
  • safeTransferFrom, che controlla se l'indirizzo di destinazione è un contratto. In tal caso, il contratto ERC-721 chiede al contratto ricevente se desidera ricevere l'NFT.

Per rispondere alle richieste safeTransferFrom, un contratto ricevente deve implementare ERC721Receiver.

1 _operator: address,
2 _from: address,

L'indirizzo _from è l'attuale proprietario del token. L'indirizzo _operator è quello che ha richiesto il trasferimento (questi due potrebbero non essere gli stessi, a causa delle autorizzazioni).

1 _tokenId: uint256,

Gli ID dei token ERC-721 sono a 256 bit. In genere vengono creati eseguendo l'hash di una descrizione di ciò che il token rappresenta.

1 _data: Bytes[1024]

La richiesta può contenere fino a 1024 byte di dati utente.

1 ) -> bytes32: view

Per prevenire i casi in cui un contratto accetta accidentalmente un trasferimento, il valore di ritorno non è un booleano, ma 256 bit con un valore specifico.

Questa funzione è una view, il che significa che può leggere lo stato della blockchain, ma non modificarlo.

Eventi

Gli eventi (opens in a new tab) vengono emessi per informare gli utenti e i server all'esterno della blockchain degli eventi. Nota che il contenuto degli eventi non è disponibile per i contratti sulla blockchain.

1# @dev Emesso quando la proprietà di qualsiasi NFT cambia tramite qualsiasi meccanismo. Questo evento viene emesso quando gli NFT vengono
2# creati (`from` == 0) e distrutti (`to` == 0). Eccezione: durante la creazione del contratto, qualsiasi
3# numero di NFT può essere creato e assegnato senza emettere Transfer. Al momento di qualsiasi
4# trasferimento, l'indirizzo approvato per quell'NFT (se presente) viene reimpostato a nessuno.
5# @param _from Mittente dell'NFT (se l'indirizzo è l'indirizzo zero indica la creazione del token).
6# @param _to Ricevitore dell'NFT (se l'indirizzo è l'indirizzo zero indica la distruzione del token).
7# @param _tokenId L'NFT che è stato trasferito.
8event Transfer:
9 sender: indexed(address)
10 receiver: indexed(address)
11 tokenId: indexed(uint256)

Questo è simile all'evento Transfer dell'ERC-20, tranne per il fatto che riportiamo un tokenId invece di un importo. Nessuno possiede l'indirizzo zero, quindi per convenzione lo usiamo per segnalare la creazione e la distruzione dei token.

1# @dev Questo viene emesso quando l'indirizzo approvato per un NFT viene modificato o riaffermato. L'indirizzo
2# zero indica che non c'è alcun indirizzo approvato. Quando viene emesso un evento Transfer, questo
3# indica anche che l'indirizzo approvato per quell'NFT (se presente) viene reimpostato a nessuno.
4# @param _owner Proprietario dell'NFT.
5# @param _approved Indirizzo che stiamo approvando.
6# @param _tokenId NFT che stiamo approvando.
7event Approval:
8 owner: indexed(address)
9 approved: indexed(address)
10 tokenId: indexed(uint256)

Un'approvazione ERC-721 è simile a un'autorizzazione (allowance) ERC-20. A un indirizzo specifico è consentito trasferire un token specifico. Questo fornisce un meccanismo ai contratti per rispondere quando accettano un token. I contratti non possono ascoltare gli eventi, quindi se trasferisci semplicemente il token a loro, non ne "sanno" nulla. In questo modo il proprietario invia prima un'approvazione e poi invia una richiesta al contratto: "Ho approvato il trasferimento del token X da parte tua, per favore procedi...".

Questa è una scelta di progettazione per rendere lo standard ERC-721 simile allo standard ERC-20. Poiché i token ERC-721 non sono fungibili, un contratto può anche identificare di aver ottenuto un token specifico esaminando la proprietà del token.

1# @dev Questo viene emesso quando un operatore viene abilitato o disabilitato per un proprietario. L'operatore può gestire
2# tutti gli NFT del proprietario.
3# @param _owner Proprietario dell'NFT.
4# @param _operator Indirizzo al quale stiamo impostando i diritti di operatore.
5# @param _approved Stato dei diritti dell'operatore (true se i diritti dell'operatore sono concessi e false se
6# revocati).
7event ApprovalForAll:
8 owner: indexed(address)
9 operator: indexed(address)
10 approved: bool

A volte è utile avere un operatore che possa gestire tutti i token di un account di un tipo specifico (quelli gestiti da un contratto specifico), in modo simile a una procura. Ad esempio, potrei voler dare tale potere a un contratto che controlla se non l'ho contattato per sei mesi e, in tal caso, distribuisce i miei beni ai miei eredi (se uno di loro lo chiede, i contratti non possono fare nulla senza essere chiamati da una transazione). Nell'ERC-20 possiamo semplicemente dare un'alta autorizzazione a un contratto di eredità, ma questo non funziona per l'ERC-721 perché i token non sono fungibili. Questo è l'equivalente.

Il valore approved ci dice se l'evento riguarda un'approvazione o il ritiro di un'approvazione.

Variabili di stato

Queste variabili contengono lo stato attuale dei token: quali sono disponibili e chi li possiede. La maggior parte di questi sono oggetti HashMap, mappature unidirezionali che esistono tra due tipi (opens in a new tab).

1# @dev Mappatura dall'ID dell'NFT all'indirizzo che lo possiede.
2idToOwner: HashMap[uint256, address]
3
4# @dev Mappatura dall'ID dell'NFT all'indirizzo approvato.
5idToApprovals: HashMap[uint256, address]

Le identità degli utenti e dei contratti in Ethereum sono rappresentate da indirizzi a 160 bit. Queste due variabili mappano dagli ID dei token ai loro proprietari e a coloro che sono approvati per trasferirli (al massimo uno per ciascuno). In Ethereum, i dati non inizializzati sono sempre zero, quindi se non c'è un proprietario o un trasferitore approvato, il valore per quel token è zero.

1# @dev Mappatura dall'indirizzo del proprietario al conteggio dei suoi token.
2ownerToNFTokenCount: HashMap[address, uint256]

Questa variabile contiene il conteggio dei token per ogni proprietario. Non c'è alcuna mappatura dai proprietari ai token, quindi l'unico modo per identificare i token posseduti da un proprietario specifico è guardare indietro nella cronologia degli eventi della blockchain e vedere gli eventi Transfer appropriati. Possiamo usare questa variabile per sapere quando abbiamo tutti gli NFT e non abbiamo bisogno di guardare ancora più indietro nel tempo.

Nota che questo algoritmo funziona solo per le interfacce utente e i server esterni. Il codice in esecuzione sulla blockchain stessa non può leggere gli eventi passati.

1# @dev Mappatura dall'indirizzo del proprietario alla mappatura degli indirizzi degli operatori.
2ownerToOperators: HashMap[address, HashMap[address, bool]]

Un account può avere più di un singolo operatore. Una semplice HashMap è insufficiente per tenerne traccia, perché ogni chiave porta a un singolo valore. Invece, puoi usare HashMap[address, bool] come valore. Per impostazione predefinita, il valore per ogni indirizzo è False, il che significa che non è un operatore. Puoi impostare i valori su True secondo necessità.

1# @dev Indirizzo del minter, che può coniare un token
2minter: address

I nuovi token devono essere creati in qualche modo. In questo contratto c'è una singola entità a cui è consentito farlo, il minter (coniatore). Questo è probabilmente sufficiente per un gioco, ad esempio. Per altri scopi, potrebbe essere necessario creare una logica di business più complicata.

1# @dev Mappatura dell'id dell'interfaccia a bool per indicare se è supportata o meno
2supportedInterfaces: HashMap[bytes32, bool]
3
4# @dev ID dell'interfaccia ERC165 di ERC165
5ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7
6
7# @dev ID dell'interfaccia ERC165 di ERC721
8ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd

L'ERC-165 (opens in a new tab) specifica un meccanismo per un contratto per rivelare come le applicazioni possono comunicare con esso, a quali ERC è conforme. In questo caso, il contratto è conforme a ERC-165 ed ERC-721.

Funzioni

Queste sono le funzioni che implementano effettivamente l'ERC-721.

Costruttore

1@external
2def __init__():

In Vyper, come in Python, la funzione costruttore è chiamata __init__.

1 # @dev Costruttore del contratto.
2
3
4

In Python e in Vyper, puoi anche creare un commento specificando una stringa multilinea (che inizia e finisce con """) e non utilizzandola in alcun modo. Questi commenti possono includere anche 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

Per accedere alle variabili di stato si usa self.<nome variabile> (di nuovo, come in Python).

Funzioni View

Queste sono funzioni che non modificano lo stato della blockchain e, pertanto, possono essere eseguite gratuitamente se chiamate esternamente. Se le funzioni view vengono chiamate da un contratto, devono comunque essere eseguite su ogni nodo e quindi costano gas.

1@view
2@external

Queste parole chiave prima della definizione di una funzione che iniziano con una chiocciola (@) sono chiamate decorazioni. Specificano le circostanze in cui una funzione può essere chiamata.

  • @view specifica che questa funzione è una view.
  • @external specifica che questa particolare funzione può essere chiamata dalle transazioni e da altri contratti.
1def supportsInterface(_interfaceID: bytes32) -> bool:

A differenza di Python, Vyper è un linguaggio a tipizzazione statica (opens in a new tab). Non puoi dichiarare una variabile o un parametro di funzione senza identificare il tipo di dato (opens in a new tab). In questo caso il parametro di input è bytes32, un valore a 256 bit (256 bit è la dimensione della parola nativa della macchina virtuale di Ethereum). L'output è un valore booleano. Per convenzione, i nomi dei parametri di funzione iniziano con un trattino basso (_).

1 # @dev L'identificazione dell'interfaccia è specificata in ERC-165.
2 @param _interfaceID Id dell'interfaccia
3
4
5
6
7 return self.supportedInterfaces[_interfaceID]

Restituisce il valore dalla HashMap self.supportedInterfaces, che è impostata nel costruttore (__init__).

1# ## FUNZIONI DI VISUALIZZAZIONE ###

Queste sono le funzioni view che rendono disponibili le informazioni sui token agli utenti e ad altri contratti.

1@view
2@external
3def balanceOf(_owner: address) -> uint256:
4 # @dev Restituisce il numero di NFT posseduti da `_owner`.
5 Genera un'eccezione se `_owner` è l'indirizzo zero. Gli NFT assegnati all'indirizzo zero sono considerati non validi.
6 @param _owner Indirizzo per il quale interrogare il saldo.
7
8
9
10
11
12 assert _owner != ZERO_ADDRESS

Questa riga asserisce (opens in a new tab) che _owner non è zero. Se lo è, c'è un errore e l'operazione viene annullata (reverted).

1 return self.ownerToNFTokenCount[_owner]
2
3@view
4@external
5def ownerOf(_tokenId: uint256) -> address:
6 # @dev Restituisce l'indirizzo del proprietario dell'NFT.
7 Genera un'eccezione se `_tokenId` non è un NFT valido.
8 @param _tokenId L'identificatore per un NFT.
9
10
11
12
13
14 owner: address = self.idToOwner[_tokenId]
15 # Genera un'eccezione se `_tokenId` non è un NFT valido
16 assert owner != ZERO_ADDRESS
17 return owner

Nella macchina virtuale di Ethereum (EVM) qualsiasi spazio di archiviazione che non ha un valore memorizzato in esso è zero. Se non c'è alcun token in _tokenId, il valore di self.idToOwner[_tokenId] è zero. In tal caso la funzione viene annullata.

1@view
2@external
3def getApproved(_tokenId: uint256) -> address:
4 # @dev Ottiene l'indirizzo approvato per un singolo NFT.
5 Genera un'eccezione se `_tokenId` non è un NFT valido.
6 @param _tokenId ID dell'NFT di cui interrogare l'approvazione.
7
8
9
10
11
12 # Genera un'eccezione se `_tokenId` non è un NFT valido
13 assert self.idToOwner[_tokenId] != ZERO_ADDRESS
14 return self.idToApprovals[_tokenId]

Nota che getApproved può restituire zero. Se il token è valido, restituisce self.idToApprovals[_tokenId]. Se non c'è alcun approvatore, quel valore è zero.

1@view
2@external
3def isApprovedForAll(_owner: address, _operator: address) -> bool:
4 # @dev Controlla se `_operator` è un operatore approvato per `_owner`.
5 @param _owner L'indirizzo che possiede gli NFT.
6 @param _operator L'indirizzo che agisce per conto del proprietario.
7
8
9
10
11
12 return (self.ownerToOperators[_owner])[_operator]

Questa funzione controlla se a _operator è consentito gestire tutti i token di _owner in questo contratto. Poiché possono esserci più operatori, questa è una HashMap a due livelli.

Funzioni di supporto al trasferimento

Queste funzioni implementano operazioni che fanno parte del trasferimento o della gestione dei token.

1
2# ## FUNZIONI DI SUPPORTO AL TRASFERIMENTO ###
3
4@view
5@internal

Questa decorazione, @internal, significa che la funzione è accessibile solo da altre funzioni all'interno dello stesso contratto. Per convenzione, anche i nomi di queste funzioni iniziano con un trattino basso (_).

1def _isApprovedOrOwner(_spender: address, _tokenId: uint256) -> bool:
2 # @dev Restituisce se lo spender specificato può trasferire un determinato ID del token
3 @param spender indirizzo dello spender da interrogare
4 @param tokenId uint256 ID del token da trasferire
5 @return bool se il msg.sender è approvato per l'ID del token specificato,
6 è un operatore del proprietario, o è il proprietario del token
7
8
9
10
11
12
13
14 owner: address = self.idToOwner[_tokenId]
15 spenderIsOwner: bool = owner == _spender
16 spenderIsApproved: bool = _spender == self.idToApprovals[_tokenId]
17 spenderIsApprovedForAll: bool = (self.ownerToOperators[owner])[_spender]
18 return (spenderIsOwner or spenderIsApproved) or spenderIsApprovedForAll

Ci sono tre modi in cui a un indirizzo può essere consentito di trasferire un token:

  1. L'indirizzo è il proprietario del token
  2. L'indirizzo è approvato per spendere quel token
  3. L'indirizzo è un operatore per il proprietario del token

La funzione precedente può essere una view perché non modifica lo stato. Per ridurre i costi operativi, qualsiasi funzione che può essere una view dovrebbe essere una view.

1@internal
2def _addTokenTo(_to: address, _tokenId: uint256):
3 # @dev Aggiunge un NFT a un determinato indirizzo
4 Genera un'eccezione se `_tokenId` è posseduto da qualcuno.
5
6
7
8
9 # Genera un'eccezione se `_tokenId` è posseduto da qualcuno
10 assert self.idToOwner[_tokenId] == ZERO_ADDRESS
11 # Cambia il proprietario
12 self.idToOwner[_tokenId] = _to
13 # Cambia il tracciamento del conteggio
14 self.ownerToNFTokenCount[_to] += 1
15
16
17@internal
18def _removeTokenFrom(_from: address, _tokenId: uint256):
19 # @dev Rimuove un NFT da un determinato indirizzo
20 Genera un'eccezione se `_from` non è il proprietario attuale.
21
22
23
24
25 # Genera un'eccezione se `_from` non è il proprietario attuale
26 assert self.idToOwner[_tokenId] == _from
27 # Cambia il proprietario
28 self.idToOwner[_tokenId] = ZERO_ADDRESS
29 # Cambia il tracciamento del conteggio
30 self.ownerToNFTokenCount[_from] -= 1

Quando c'è un problema con un trasferimento, annulliamo la chiamata.

1@internal
2def _clearApproval(_owner: address, _tokenId: uint256):
3 # @dev Cancella un'approvazione di un determinato indirizzo
4 Genera un'eccezione se `_owner` non è il proprietario attuale.
5
6
7
8
9 # Genera un'eccezione se `_owner` non è il proprietario attuale
10 assert self.idToOwner[_tokenId] == _owner
11 if self.idToApprovals[_tokenId] != ZERO_ADDRESS:
12 # Reimposta le approvazioni
13 self.idToApprovals[_tokenId] = ZERO_ADDRESS

Modifica il valore solo se necessario. Le variabili di stato risiedono nell'archiviazione (storage). Scrivere nell'archiviazione è una delle operazioni più costose che l'EVM (macchina virtuale di Ethereum) esegue (in termini di gas). Pertanto, è una buona idea ridurla al minimo; persino scrivere il valore esistente ha un costo elevato.

1@internal
2def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: address):
3 # @dev Esegue il trasferimento di un NFT.
4 Genera un'eccezione a meno che `msg.sender` non sia il proprietario attuale, un operatore autorizzato o l'indirizzo
5 approvato per questo NFT. (NOTA: `msg.sender` non è consentito in una funzione privata, quindi passa `_sender`.)
6 Genera un'eccezione se `_to` è l'indirizzo zero.
7 Genera un'eccezione se `_from` non è il proprietario attuale.
8 Genera un'eccezione se `_tokenId` non è un NFT valido.
9
10
11
12
13
14
15
16

Abbiamo questa funzione interna perché ci sono due modi per trasferire i token (normale e sicuro), ma vogliamo solo una singola posizione nel codice in cui lo facciamo per semplificare l'auditing.

1 # Controlla i requisiti
2 assert self._isApprovedOrOwner(_sender, _tokenId)
3 # Genera un'eccezione se `_to` è l'indirizzo zero
4 assert _to != ZERO_ADDRESS
5 # Cancella l'approvazione. Genera un'eccezione se `_from` non è il proprietario attuale
6 self._clearApproval(_from, _tokenId)
7 # Rimuove l'NFT. Genera un'eccezione se `_tokenId` non è un NFT valido
8 self._removeTokenFrom(_from, _tokenId)
9 # Aggiunge l'NFT
10 self._addTokenTo(_to, _tokenId)
11 # Registra il trasferimento
12 log Transfer(_from, _to, _tokenId)

Per emettere un evento in Vyper si usa un'istruzione log (vedi qui per maggiori dettagli (opens in a new tab)).

Funzioni di trasferimento

1
2# ## FUNZIONI DI TRASFERIMENTO ###
3
4@external
5def transferFrom(_from: address, _to: address, _tokenId: uint256):
6 # @dev Genera un'eccezione a meno che `msg.sender` non sia il proprietario attuale, un operatore autorizzato o l'indirizzo
7 approvato per questo NFT.
8 Genera un'eccezione se `_from` non è il proprietario attuale.
9 Genera un'eccezione se `_to` è l'indirizzo zero.
10 Genera un'eccezione se `_tokenId` non è un NFT valido.
11 @notice Il chiamante è responsabile di confermare che `_to` sia in grado di ricevere NFT, altrimenti
12 potrebbero andare persi in modo permanente.
13 @param _from Il proprietario attuale dell'NFT.
14 @param _to Il nuovo proprietario.
15 @param _tokenId L'NFT da trasferire.
16
17
18
19
20
21
22
23
24
25
26
27
28 self._transferFrom(_from, _to, _tokenId, msg.sender)

Questa funzione ti consente di trasferire a un indirizzo arbitrario. A meno che l'indirizzo non sia un utente o un contratto che sa come trasferire i token, qualsiasi token trasferito rimarrà bloccato in quell'indirizzo e sarà inutile.

1@external
2def safeTransferFrom(
3 _from: address,
4 _to: address,
5 _tokenId: uint256,
6 _data: Bytes[1024]=b""
7 ):
8 # @dev Trasferisce la proprietà di un NFT da un indirizzo a un altro indirizzo.
9 Genera un'eccezione a meno che `msg.sender` non sia il proprietario attuale, un operatore autorizzato o
10 l'indirizzo approvato per questo NFT.
11 Genera un'eccezione se `_from` non è il proprietario attuale.
12 Genera un'eccezione se `_to` è l'indirizzo zero.
13 Genera un'eccezione se `_tokenId` non è un NFT valido.
14 Se `_to` è uno smart contract, chiama `onERC721Received` su `_to` e genera un'eccezione se
15 il valore di ritorno non è `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`.
16 NOTA: bytes4 è rappresentato da bytes32 con padding
17 @param _from Il proprietario attuale dell'NFT.
18 @param _to Il nuovo proprietario.
19 @param _tokenId L'NFT da trasferire.
20 @param _data Dati aggiuntivi senza formato specificato, inviati nella chiamata a `_to`.
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 self._transferFrom(_from, _to, _tokenId, msg.sender)

Va bene eseguire prima il trasferimento perché, se c'è un problema, annulleremo comunque l'operazione, quindi tutto ciò che è stato fatto nella chiamata verrà cancellato.

1 if _to.is_contract: # controlla se `_to` è un indirizzo di contratto

Per prima cosa controlla se l'indirizzo è un contratto (se ha del codice). In caso contrario, presumi che sia un indirizzo utente e che l'utente sarà in grado di utilizzare il token o trasferirlo. Ma non farti cullare da un falso senso di sicurezza. Puoi perdere i token, anche con safeTransferFrom, se li trasferisci a un indirizzo di cui nessuno conosce la chiave privata.

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

Chiama il contratto di destinazione per vedere se può ricevere token ERC-721.

1 # Genera un'eccezione se la destinazione del trasferimento è un contratto che non implementa 'onERC721Received'
2 assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes32)

Se la destinazione è un contratto, ma non accetta token ERC-721 (o ha deciso di non accettare questo particolare trasferimento), annulla l'operazione.

1@external
2def approve(_approved: address, _tokenId: uint256):
3 # @dev Imposta o riafferma l'indirizzo approvato per un NFT. L'indirizzo zero indica che non c'è alcun indirizzo approvato.
4 Genera un'eccezione a meno che `msg.sender` non sia il proprietario attuale dell'NFT, o un operatore autorizzato del proprietario attuale.
5 Genera un'eccezione se `_tokenId` non è un NFT valido. (NOTA: Questo non è scritto nell'EIP)
6 Genera un'eccezione se `_approved` è il proprietario attuale. (NOTA: Questo non è scritto nell'EIP)
7 @param _approved Indirizzo da approvare per l'ID dell'NFT specificato.
8 @param _tokenId ID del token da approvare.
9
10
11
12
13
14
15
16
17 owner: address = self.idToOwner[_tokenId]
18 # Genera un'eccezione se `_tokenId` non è un NFT valido
19 assert owner != ZERO_ADDRESS
20 # Genera un'eccezione se `_approved` è il proprietario attuale
21 assert _approved != owner

Per convenzione, se non vuoi avere un approvatore, nomini l'indirizzo zero, non te stesso.

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

Per impostare un'approvazione puoi essere il proprietario o un operatore autorizzato dal proprietario.

1 # Imposta l'approvazione
2 self.idToApprovals[_tokenId] = _approved
3 log Approval(owner, _approved, _tokenId)
4
5
6@external
7def setApprovalForAll(_operator: address, _approved: bool):
8 # @dev Abilita o disabilita l'approvazione per una terza parte ("operatore") per gestire tutti gli
9 asset di `msg.sender`. Emette anche l'evento ApprovalForAll.
10 Genera un'eccezione se `_operator` è il `msg.sender`. (NOTA: Questo non è scritto nell'EIP)
11 @notice Questo funziona anche se il mittente non possiede alcun token in quel momento.
12 @param _operator Indirizzo da aggiungere all'insieme degli operatori autorizzati.
13 @param _approved True se l'operatore è approvato, false per revocare l'approvazione.
14
15
16
17
18
19
20
21
22 # Genera un'eccezione se `_operator` è il `msg.sender`
23 assert _operator != msg.sender
24 self.ownerToOperators[msg.sender][_operator] = _approved
25 log ApprovalForAll(msg.sender, _operator, _approved)

Coniare nuovi token e distruggere quelli esistenti

L'account che ha creato il contratto è il minter, il super utente autorizzato a coniare nuovi NFT. Tuttavia, nemmeno a lui è consentito bruciare i token esistenti. Solo il proprietario, o un'entità autorizzata dal proprietario, può farlo.

1# ## FUNZIONI PER CONIARE E BRUCIARE ###
2
3@external
4def mint(_to: address, _tokenId: uint256) -> bool:

Questa funzione restituisce sempre True, perché se l'operazione fallisce viene annullata.

1 # @dev Funzione per coniare token
2 Genera un'eccezione se `msg.sender` non è il minter.
3 Genera un'eccezione se `_to` è l'indirizzo zero.
4 Genera un'eccezione se `_tokenId` è posseduto da qualcuno.
5 @param _to L'indirizzo che riceverà i token coniati.
6 @param _tokenId L'id del token da coniare.
7 @return Un booleano che indica se l'operazione ha avuto successo.
8
9
10
11
12
13
14
15
16
17 # Genera un'eccezione se `msg.sender` non è il minter
18 assert msg.sender == self.minter

Solo il minter (l'account che ha creato il contratto ERC-721) può coniare nuovi token. Questo può essere un problema in futuro se vogliamo cambiare l'identità del minter. In un contratto di produzione probabilmente vorresti una funzione che consenta al minter di trasferire i privilegi di minter a qualcun altro.

1 # Genera un'eccezione se `_to` è l'indirizzo zero
2 assert _to != ZERO_ADDRESS
3 # Aggiunge l'NFT. Genera un'eccezione se `_tokenId` è posseduto da qualcuno
4 self._addTokenTo(_to, _tokenId)
5 log Transfer(ZERO_ADDRESS, _to, _tokenId)
6 return True

Per convenzione, il conio di nuovi token conta come un trasferimento dall'indirizzo zero.

1
2@external
3def burn(_tokenId: uint256):
4 # @dev Brucia uno specifico token ERC721.
5 Genera un'eccezione a meno che `msg.sender` non sia il proprietario attuale, un operatore autorizzato o l'indirizzo
6 approvato per questo NFT.
7 Genera un'eccezione se `_tokenId` non è un NFT valido.
8 @param _tokenId uint256 id del token ERC721 da bruciare.
9
10
11
12
13
14
15
16 # Controlla i requisiti
17 assert self._isApprovedOrOwner(msg.sender, _tokenId)
18 owner: address = self.idToOwner[_tokenId]
19 # Genera un'eccezione se `_tokenId` non è un NFT valido
20 assert owner != ZERO_ADDRESS
21 self._clearApproval(owner, _tokenId)
22 self._removeTokenFrom(owner, _tokenId)
23 log Transfer(owner, ZERO_ADDRESS, _tokenId)

Chiunque sia autorizzato a trasferire un token è autorizzato a bruciarlo. Sebbene una bruciatura appaia equivalente al trasferimento all'indirizzo zero, l'indirizzo zero in realtà non riceve il token. Questo ci consente di liberare tutto lo spazio di archiviazione che è stato utilizzato per il token, il che può ridurre il costo del gas della transazione.

Utilizzare questo contratto

A differenza di Solidity, Vyper non ha l'ereditarietà. Questa è una scelta di progettazione deliberata per rendere il codice più chiaro e quindi più facile da proteggere. Quindi, per creare il tuo contratto ERC-721 in Vyper, prendi questo contratto (opens in a new tab) e modificalo per implementare la logica di business che desideri.

Conclusione

Per riepilogare, ecco alcune delle idee più importanti in questo contratto:

  • Per ricevere token ERC-721 con un trasferimento sicuro, i contratti devono implementare l'interfaccia ERC721Receiver.
  • Anche se usi il trasferimento sicuro, i token possono comunque rimanere bloccati se li invii a un indirizzo di cui non si conosce la chiave privata.
  • Quando c'è un problema con un'operazione, è una buona idea annullare (revert) la chiamata, piuttosto che restituire semplicemente un valore di fallimento.
  • I token ERC-721 esistono quando hanno un proprietario.
  • Ci sono tre modi per essere autorizzati a trasferire un NFT. Puoi essere il proprietario, essere approvato per un token specifico, o essere un operatore per tutti i token del proprietario.
  • Gli eventi passati sono visibili solo all'esterno della blockchain. Il codice in esecuzione all'interno della blockchain non può visualizzarli.

Ora vai e implementa contratti Vyper sicuri.

Vedi qui per altri miei lavori (opens in a new tab).

Ultimo aggiornamento della pagina: 3 marzo 2026

Questo tutorial è stato utile?