Passer au contenu principal

Découvrir le contrat Vyper ERC-721

Vyper
erc-721
Python
Débutant
Ori Pomerantz
1 avril 2021
22 minutes de lecture

Introduction

La norme ERC-721 est utilisée pour détenir la propriété des jetons non fongibles (NFT). Les jetons ERC-20 se comportent comme un produit de base, car il n'y a aucune différence entre les jetons individuels. En revanche, les jetons ERC-721 sont conçus pour des actifs similaires mais non identiques, tels que différents dessins animés de chats (opens in a new tab) ou des titres de propriété pour différents biens immobiliers.

Dans cet article, nous analyserons le contrat ERC-721 de Ryuya Nakamura (opens in a new tab). Ce contrat est écrit en Vyper (opens in a new tab), un langage de contrat de type Python conçu pour rendre plus difficile l'écriture de code non sécurisé qu'en Solidity.

Le contrat

# @dev Implémentation de la norme de jeton non fongible ERC-721.
# @author Ryuya Nakamura (@nrryuya)
# Modifié à partir de : https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy

Les commentaires en Vyper, comme en Python, commencent par un dièse (#) et se poursuivent jusqu'à la fin de la ligne. Les commentaires qui incluent @<keyword> sont utilisés par NatSpec (opens in a new tab) pour produire une documentation lisible par l'homme.

from vyper.interfaces import ERC721

implements: ERC721

L'interface ERC-721 est intégrée au langage Vyper. Vous pouvez voir la définition du code ici (opens in a new tab). La définition de l'interface est écrite en Python plutôt qu'en Vyper, car les interfaces ne sont pas seulement utilisées au sein de la blockchain, mais aussi lors de l'envoi d'une transaction à la blockchain depuis un client externe, qui peut être écrit en Python.

La première ligne importe l'interface, et la deuxième spécifie que nous l'implémentons ici.

L'interface ERC721Receiver

# Interface pour le contrat appelé par safeTransferFrom()
interface ERC721Receiver:
    def onERC721Received(

L'ERC-721 prend en charge deux types de transfert :

  • transferFrom, qui permet à l'expéditeur de spécifier n'importe quelle adresse de destination et qui lui attribue la responsabilité du transfert. Cela signifie que vous pouvez effectuer un transfert vers une adresse invalide, auquel cas le NFT est perdu à jamais.
  • safeTransferFrom, qui vérifie si l'adresse de destination est un contrat. Si c'est le cas, le contrat ERC-721 demande au contrat destinataire s'il souhaite recevoir le NFT.

Pour répondre aux requêtes safeTransferFrom, un contrat destinataire doit implémenter ERC721Receiver.

            _operator: address,
            _from: address,

L'adresse _from est le propriétaire actuel du jeton. L'adresse _operator est celle qui a demandé le transfert (ces deux adresses peuvent ne pas être les mêmes, en raison des autorisations).

            _tokenId: uint256,

Les ID de jetons ERC-721 sont de 256 bits. Généralement, ils sont créés en effectuant un hachage de la description de ce que le jeton représente.

            _data: Bytes[1024]

La requête peut contenir jusqu'à 1024 octets de données utilisateur.

        ) -> bytes32: view

Pour éviter les cas où un contrat accepte accidentellement un transfert, la valeur de retour n'est pas un booléen, mais une valeur de 256 bits avec une valeur spécifique.

Cette fonction est une view, ce qui signifie qu'elle peut lire l'état de la blockchain, mais pas le modifier.

Événements

Des événements (opens in a new tab) sont émis pour informer les utilisateurs et les serveurs en dehors de la blockchain. Notez que le contenu des événements n'est pas disponible pour les contrats sur la blockchain.

C'est similaire à l'événement Transfer de l'ERC-20, sauf que nous déclarons un tokenId au lieu d'un montant. Personne ne possède l'adresse zéro, donc par convention, nous l'utilisons pour signaler la création et la destruction des jetons.

Une approbation ERC-721 est similaire à une autorisation ERC-20. Une adresse spécifique est autorisée à transférer un jeton spécifique. Cela fournit un mécanisme permettant aux contrats de répondre lorsqu'ils acceptent un jeton. Les contrats ne peuvent pas écouter les événements, donc si vous leur transférez simplement le jeton, ils n'en seront pas « informés ». De cette façon, le propriétaire soumet d'abord une approbation, puis envoie une demande au contrat : « Je vous ai autorisé à transférer le jeton X, veuillez le faire... ».

Il s'agit d'un choix de conception visant à rendre la norme ERC-721 similaire à la norme ERC-20. Comme les jetons ERC-721 ne sont pas fongibles, un contrat peut également identifier qu'il a reçu un jeton spécifique en consultant la propriété du jeton.

Il est parfois utile de disposer d'un opérateur qui peut gérer tous les jetons d'un compte d'un type spécifique (ceux qui sont gérés par un contrat spécifique), à la manière d'une procuration. Par exemple, je pourrais vouloir donner un tel pouvoir à un contrat qui vérifie si je ne l'ai pas contacté depuis six mois et, si c'est le cas, distribue mes actifs à mes héritiers (si l'un d'eux le demande, car les contrats ne peuvent rien faire sans être appelés par une transaction). Avec l'ERC-20, nous pouvons simplement donner une autorisation élevée à un contrat d'héritage, mais cela ne fonctionne pas pour l'ERC-721 car les jetons ne sont pas fongibles. C'est l'équivalent.

La valeur approved nous indique si l'événement concerne une approbation ou le retrait d'une approbation.

Variables d'état

Ces variables contiennent l'état actuel des jetons : lesquels sont disponibles et qui les possède. La plupart d'entre elles sont des objets HashMap, des mappages unidirectionnels qui existent entre deux types (opens in a new tab).

# @dev Mappage de l'ID du NFT à l'adresse qui le possède.
idToOwner: HashMap[uint256, address]

# @dev Mappage de l'ID du NFT à l'adresse approuvée.
idToApprovals: HashMap[uint256, address]

Les identités des utilisateurs et des contrats dans Ethereum sont représentées par des adresses de 160 bits. Ces deux variables mappent les ID des jetons à leurs propriétaires et à ceux approuvés pour les transférer (au maximum un pour chaque). Dans Ethereum, les données non initialisées sont toujours nulles, donc s'il n'y a pas de propriétaire ou de transféreur approuvé, la valeur de ce jeton est nulle.

# @dev Mappage de l'adresse du propriétaire au nombre de ses jetons.
ownerToNFTokenCount: HashMap[address, uint256]

Cette variable contient le nombre de jetons pour chaque propriétaire. Il n'y a pas de mappage des propriétaires vers les jetons, donc la seule façon d'identifier les jetons qu'un propriétaire spécifique possède est de consulter l'historique des événements de la blockchain et de voir les événements Transfer appropriés. Nous pouvons utiliser cette variable pour savoir quand nous avons tous les NFT et que nous n'avons pas besoin de chercher plus loin dans le temps.

Notez que cet algorithme ne fonctionne que pour les interfaces utilisateur et les serveurs externes. Le code s'exécutant sur la blockchain elle-même ne peut pas lire les événements passés.

# @dev Mappage de l'adresse du propriétaire au mappage des adresses des opérateurs.
ownerToOperators: HashMap[address, HashMap[address, bool]]

Un compte peut avoir plus d'un opérateur. Un HashMap simple est insuffisant pour en garder la trace, car chaque clé mène à une seule valeur. À la place, vous pouvez utiliser HashMap[address, bool] comme valeur. Par défaut, la valeur de chaque adresse est False, ce qui signifie qu'il ne s'agit pas d'un opérateur. Vous pouvez définir les valeurs à True si nécessaire.

# @dev Adresse du minter, qui peut frapper un jeton
minter: address

Les nouveaux jetons doivent être créés d'une manière ou d'une autre. Dans ce contrat, il n'y a qu'une seule entité autorisée à le faire, le minter. Ceci est probablement suffisant pour un jeu, par exemple. À d'autres fins, il pourrait être nécessaire de créer une logique métier plus compliquée.

# @dev Mappage de l'ID de l'interface à un booléen indiquant si elle est prise en charge ou non
supportedInterfaces: HashMap[bytes32, bool]

# @dev ID d'interface ERC165 de ERC165
ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7

# @dev ID d'interface ERC165 de ERC721
ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd

L'ERC-165 (opens in a new tab) spécifie un mécanisme permettant à un contrat de divulguer la manière dont les applications peuvent communiquer avec lui et à quelles normes ERC il se conforme. Dans ce cas, le contrat est conforme aux normes ERC-165 et ERC-721.

Fonctions

Ce sont les fonctions qui implémentent réellement l'ERC-721.

Constructeur

@external
def __init__():

En Vyper, comme en Python, la fonction constructeur est appelée __init__.

    """
    @dev Constructeur de contrat.
    """

En Python et en Vyper, vous pouvez également créer un commentaire en spécifiant une chaîne multiligne (qui commence et se termine par """), sans l'utiliser d'aucune façon. Ces commentaires peuvent également inclure NatSpec (opens in a new tab).

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

Pour accéder aux variables d'état, vous utilisez self.<nom de la variable>(encore une fois, comme en Python).

Fonctions de vue

Ce sont des fonctions qui ne modifient pas l'état de la blockchain et qui peuvent donc être exécutées gratuitement si elles sont appelées en externe. Si les fonctions de vue sont appelées par un contrat, elles doivent tout de même être exécutées sur chaque nœud et coûtent donc du gaz.

@view
@external

Ces mots-clés précédant une définition de fonction qui commencent par un signe « at » (@) sont appelés des décorations. Ils spécifient les circonstances dans lesquelles une fonction peut être appelée.

  • @view spécifie que cette fonction est une vue.
  • @external spécifie que cette fonction particulière peut être appelée par des transactions et par d'autres contrats.
def supportsInterface(_interfaceID: bytes32) -> bool:

Contrairement à Python, Vyper est un langage à typage statique (opens in a new tab). Vous ne pouvez pas déclarer une variable, ou un paramètre de fonction, sans identifier le type de données (opens in a new tab). Dans ce cas, le paramètre d'entrée est bytes32, une valeur de 256 bits (256 bits est la taille de mot native de la machine virtuelle Ethereum). La sortie est une valeur booléenne. Par convention, les noms des paramètres de fonction commencent par un trait de soulignement (_).

    """
    @dev L'identification de l'interface est spécifiée dans l'ERC-165.
    @param _interfaceID ID de l'interface
    """
    return self.supportedInterfaces[_interfaceID]

Retourne la valeur du HashMap self.supportedInterfaces, qui est définie dans le constructeur (__init__).

### FONCTIONS DE VUE ###

Ce sont les fonctions de vue qui rendent les informations sur les jetons disponibles pour les utilisateurs et autres contrats.

Cette ligne affirme (opens in a new tab) que _owner n'est pas zéro. Si c'est le cas, il y a une erreur et l'opération est annulée.

Dans la machine virtuelle Ethereum (EVM), tout stockage qui ne contient pas de valeur stockée est égal à zéro. S'il n'y a pas de jeton à _tokenId, alors la valeur de self.idToOwner[_tokenId] est zéro. Dans ce cas, la fonction est annulée.

Notez que getApproved peut retourner zéro. Si le jeton est valide, il retourne self.idToApprovals[_tokenId]. S'il n'y a pas d'approbateur, cette valeur est zéro.

Cette fonction vérifie si _operator est autorisé à gérer tous les jetons de _owner dans ce contrat. Comme il peut y avoir plusieurs opérateurs, il s'agit d'un HashMap à deux niveaux.

Fonctions d'aide au transfert

Ces fonctions implémentent des opérations qui font partie du transfert ou de la gestion des jetons.


### FONCTIONS D'AIDE AU TRANSFERT ###

@view
@internal

Cette décoration, @internal, signifie que la fonction n'est accessible qu'à partir d'autres fonctions du même contrat. Par convention, ces noms de fonction commencent également par un trait de soulignement (_).

Il y a trois façons pour une adresse d'être autorisée à transférer un jeton :

  1. L'adresse est le propriétaire du jeton
  2. L'adresse est approuvée pour dépenser ce jeton
  3. L'adresse est un opérateur pour le propriétaire du jeton

La fonction ci-dessus peut être une vue car elle ne modifie pas l'état. Pour réduire les coûts d'exploitation, toute fonction qui peut être une vue devrait être une vue.

En cas de problème avec un transfert, nous annulons l'appel.

Ne modifiez la valeur que si nécessaire. Les variables d'état vivent dans le stockage. L'écriture dans le stockage est l'une des opérations les plus coûteuses que l'EVM (machine virtuelle Ethereum) effectue (en termes de gaz). Par conséquent, il est conseillé de la minimiser, même l'écriture de la valeur existante a un coût élevé.

Nous avons cette fonction interne car il y a deux façons de transférer des jetons (régulière et sûre), mais nous ne voulons qu'un seul emplacement dans le code où nous le faisons pour faciliter l'audit.

Pour émettre un événement en Vyper, vous utilisez une instruction log (voir ici pour plus de détails (opens in a new tab)).

Fonctions de transfert

Cette fonction vous permet de transférer vers une adresse arbitraire. À moins que l'adresse ne soit un utilisateur ou un contrat qui sait comment transférer des jetons, tout jeton que vous transférez sera bloqué dans cette adresse et inutilisable.

Il est acceptable de faire le transfert en premier car en cas de problème, nous allons de toute façon annuler, donc tout ce qui a été fait dans l'appel sera annulé.

    if _to.is_contract: # vérifier si `_to` est une adresse de contrat

Vérifiez d'abord si l'adresse est un contrat (si elle a du code). Sinon, supposez qu'il s'agit d'une adresse d'utilisateur et que l'utilisateur pourra utiliser le jeton ou le transférer. Mais ne vous laissez pas bercer par un faux sentiment de sécurité. Vous pouvez perdre des jetons, même avec safeTransferFrom, si vous les transférez à une adresse dont personne ne connaît la clé privée.

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

Appelez le contrat cible pour voir s'il peut recevoir des jetons ERC-721.

        # Lance une exception si la destination du transfert est un contrat qui n'implémente pas 'onERC721Received'
        assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes32)

Si la destination est un contrat, mais qui n'accepte pas les jetons ERC-721 (ou qui a décidé de ne pas accepter ce transfert particulier), annulez.

Par convention, si vous ne voulez pas avoir d'approbateur, vous désignez l'adresse zéro, et non vous-même.

    # Vérifier les exigences
    senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender
    senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender]
    assert (senderIsOwner or senderIsApprovedForAll)

Pour définir une approbation, vous pouvez être soit le propriétaire, soit un opérateur autorisé par le propriétaire.

Frapper de nouveaux jetons et détruire ceux existants

Le compte qui a créé le contrat est le minter, le super utilisateur autorisé à frapper de nouveaux NFT. Cependant, même lui n'est pas autorisé à détruire des jetons existants. Seul le propriétaire, ou une entité autorisée par le propriétaire, peut le faire.

### FONCTIONS DE FRAPPE ET DE DESTRUCTION ###

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

Cette fonction retourne toujours True, car si l'opération échoue, elle est annulée.

Seul le minter (le compte qui a créé le contrat ERC-721) peut frapper de nouveaux jetons. Cela peut poser un problème à l'avenir si nous voulons changer l'identité du minter. Dans un contrat en production, vous voudriez probablement une fonction qui permet au minter de transférer les privilèges de minter à quelqu'un d'autre.

    # Lance une exception si `_to` est l'adresse zéro
    assert _to != ZERO_ADDRESS
    # Ajoute un NFT. Lance une exception si `_tokenId` est détenu par quelqu'un
    self._addTokenTo(_to, _tokenId)
    log Transfer(ZERO_ADDRESS, _to, _tokenId)
    return True

Par convention, la frappe de nouveaux jetons est considérée comme un transfert depuis l'adresse zéro.

Toute personne autorisée à transférer un jeton est autorisée à le détruire. Bien que la destruction d'un jeton semble équivalente à un transfert vers l'adresse zéro, l'adresse zéro ne reçoit pas réellement le jeton. Cela nous permet de libérer tout le stockage qui a été utilisé pour le jeton, ce qui peut réduire le coût en gaz de la transaction.

Utilisation de ce contrat

Contrairement à Solidity, Vyper n'a pas d'héritage. C'est un choix de conception délibéré pour rendre le code plus clair et donc plus facile à sécuriser. Donc, pour créer votre propre contrat Vyper ERC-721, vous prenez ce contrat (opens in a new tab) et le modifiez pour implémenter la logique métier que vous souhaitez.

Conclusion

Pour récapituler, voici quelques-unes des idées les plus importantes de ce contrat :

  • Pour recevoir des jetons ERC-721 avec un transfert sécurisé, les contrats doivent implémenter l'interface ERC721Receiver.
  • Même si vous utilisez un transfert sécurisé, les jetons peuvent toujours être bloqués si vous les envoyez à une adresse dont la clé privée est inconnue.
  • En cas de problème avec une opération, il est conseillé d'annuler (revert) l'appel, plutôt que de simplement retourner une valeur d'échec.
  • Les jetons ERC-721 existent lorsqu'ils ont un propriétaire.
  • Il existe trois façons d'être autorisé à transférer un NFT. Vous pouvez être le propriétaire, être approuvé pour un jeton spécifique, ou être un opérateur pour tous les jetons du propriétaire.
  • Les événements passés ne sont visibles qu'en dehors de la blockchain. Le code exécuté à l'intérieur de la blockchain ne peut pas les voir.

Maintenant, allez implémenter des contrats Vyper sécurisés.

Voir ici pour plus de mon travail (opens in a new tab).

Dernière mise à jour de la page : 28 avril 2026

Ce tutoriel vous a-t-il été utile ?