Saltar al contenido principal

Recorrido por un contrato ERC-721 de Vyper

vyper
erc-721
python
Principiante
Ori Pomerantz
1 de abril de 2021
22 minuto leído

Introducción

El estándar ERC-721 se usa para mantener la propiedad de los tokens no fungibles (NFT). Los tókenes ERC-20 se comportan como una mercancía, porque no hay diferencia entre los tókenes individuales. A diferencia de esto, los tókenes ERC-721 están diseñados para activos que son similares pero no idénticos, como diferentes dibujos de gatos (opens in a new tab) o títulos de diferentes propiedades inmobiliarias.

En este artículo analizaremos el contrato ERC-721 de Ryuya Nakamura (opens in a new tab). Este contrato está escrito en Vyper (opens in a new tab), un lenguaje de contratos similar a Python diseñado para que sea más difícil escribir código inseguro que en Solidity.

El contrato

# @dev Implementación del estándar de token no fungible ERC-721.
# @author Ryuya Nakamura (@nrryuya)
# Modificado de: https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy

Los comentarios en Vyper, como en Python, empiezan con un hash (#) y continúan hasta el final de la línea. Los comentarios que incluyen @<keyword> son utilizados por NatSpec (opens in a new tab) para producir documentación legible por humanos.

from vyper.interfaces import ERC721

implements: ERC721

La interfaz ERC-721 está integrada en el lenguaje Vyper. Puede ver la definición del código aquí (opens in a new tab). La definición de la interfaz está escrita en Python, en lugar de Vyper, porque las interfaces se utilizan no solo dentro de la cadena de bloques, sino también al enviar a la cadena de bloques una transacción desde un cliente externo, que puede estar escrito en Python.

La primera línea importa la interfaz y la segunda especifica que la estamos implementando aquí.

La interfaz ERC721Receiver

# Interfaz para el contrato llamado por safeTransferFrom()
interface ERC721Receiver:
    def onERC721Received(

ERC-721 admite dos tipos de transferencia:

  • transferFrom, que permite al emisor especificar cualquier dirección de destino y pone la responsabilidad de la transferencia sobre el emisor. Esto significa que puede transferir a una dirección no válida, en cuyo caso el NFT se pierde para siempre.
  • safeTransferFrom, que comprueba si la dirección de destino es un contrato. Si es así, el contrato ERC-721 le pregunta al contrato receptor si quiere recibir el NFT.

Para responder a las solicitudes de safeTransferFrom, un contrato receptor tiene que implementar ERC721Receiver.

            _operator: address,
            _from: address,

La dirección _from es el propietario actual del token. La dirección _operator es la que solicitó la transferencia (estas dos pueden no ser las mismas, debido a los permisos).

            _tokenId: uint256,

Los ID del token ERC-721 son de 256 bits. Normalmente, se crean mediante el hash de una descripción de lo que el token representa.

            _data: Bytes[1024]

La solicitud puede tener hasta 1024 bytes de datos de usuario.

        ) -> bytes32: view

Para evitar casos en los que un contrato acepte accidentalmente una transferencia, el valor de retorno no es un booleano, sino 256 bits con un valor específico.

Esta función es una view, lo que significa que puede leer el estado de la cadena de bloques, pero no modificarlo.

Eventos

Se emiten eventos (opens in a new tab) para informar de los eventos a los usuarios y servidores que están fuera de la cadena de bloques. Tenga en cuenta que el contenido de los eventos no está disponible para los contratos en la cadena de bloques.

Esto es similar al evento de Transferencia de ERC-20, con la excepción de que se reporta un tokenId en lugar de una cantidad. Nadie posee la dirección cero, así que, por convención, la usamos para informar de la creación y la destrucción de los tokens.

Una aprobación de ERC-721 es similar a una autorización de ERC-20. A una dirección específica se le permite transferir un token específico. Esto proporciona un mecanismo para que los contratos respondan cuando aceptan un token. Los contratos no pueden escuchar eventos, así que si simplemente les transfiere el token, estos no lo "sabrán". De esta manera, el propietario primero envía una aprobación y luego una solicitud al contrato: "Le he aprobado para transferir el token X, por favor, hágalo...".

Esta es una decisión de diseño para hacer que el estándar ERC-721 sea similar al estándar ERC-20. Como los tókenes ERC-721 no son fungibles, un contrato también puede identificar que obtuvo un token específico al mirar la propiedad del token.

A veces es útil tener un operador que pueda gestionar todos los tókenes de una cuenta de un tipo específico (aquellos que son gestionados por un contrato específico), de forma similar a un poder notarial. Por ejemplo, podría querer darle dicho poder a un contrato que compruebe si no lo he contactado durante seis meses y, en ese caso, distribuya mis activos a mis herederos (si uno de ellos lo solicita, los contratos no pueden hacer nada sin ser llamados por una transacción). En ERC-20 podemos simplemente dar una autorización alta a un contrato de herencia, pero eso no funciona para ERC-721 porque los tókenes no son fungibles. Este es el equivalente.

El valor approved nos dice si el evento es para una aprobación o para el retiro de una aprobación.

Variables de estado

Estas variables contienen el estado actual de los tókenes: cuáles están disponibles y quién los posee. La mayoría de estos son objetos HashMap, mapeos unidireccionales que existen entre dos tipos (opens in a new tab).

# @dev Mapeo del ID del NFT a la dirección que lo posee.
idToOwner: HashMap[uint256, address]

# @dev Mapeo del ID del NFT a la dirección aprobada.
idToApprovals: HashMap[uint256, address]

Las identidades del usuario y del contrato en Ethereum vienen representadas por direcciones de 160 bits. Estas dos variables mapean desde identificadores de tókenes a sus propietarios y aquellos aprobados para transferirlos (máximo uno para cada uno). En Ethereum, los datos no inicializados siempre son cero, así que si no hay ningún propietario o transferidor aprobado, el valor para ese token es cero.

# @dev Mapeo de la dirección del propietario al recuento de sus tokens.
ownerToNFTokenCount: HashMap[address, uint256]

Esta variable contiene el recuento de tokens de cada propietario. No hay mapeo de propietarios a tókenes, así que la única manera de identificar los tókenes que posee un propietario específico es mirar el historial de eventos de la cadena de bloques y ver los eventos apropiados de Transfer. Podemos usar esta variable para saber cuándo tenemos todos los NFT y no necesitamos buscar más atrás en el tiempo.

Tenga en cuenta que este algoritmo sólo funciona para interfaces de usuario y servidores externos. El código en ejecución en la propia cadena de bloques no puede leer eventos pasados.

# @dev Mapeo de la dirección del propietario al mapeo de las direcciones del operador.
ownerToOperators: HashMap[address, HashMap[address, bool]]

Una cuenta puede tener más de un solo operador. Un HashMap simple es insuficiente para llevar un seguimiento de ellos, porque cada clave conduce a un único valor. En su lugar, puede utilizar HashMap[address, bool] como el valor. Por defecto, el valor para cada dirección es False, lo que significa que no es un operador. Puede establecer los valores a True según sea necesario.

# @dev Dirección del acuñador, quien puede acuñar un token.
minter: address

Los nuevos tokens tienen que crearse de alguna manera. En este contrato hay una única entidad que puede hacerlo, el minter. Es probable que esto sea suficiente para un juego, por ejemplo. Para otros propósitos, podría ser necesario crear una lógica de negocio más complicada.

# @dev Mapeo de la id de la interfaz a un bool sobre si es compatible o no.
supportedInterfaces: HashMap[bytes32, bool]

# @dev ID de interfaz ERC165 de ERC165
ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7

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

ERC-165 (opens in a new tab) especifica un mecanismo para que un contrato revele cómo las aplicaciones pueden comunicarse con él y a qué ERC se ajusta. En este caso, el contrato se ajusta a ERC-165 y ERC-721.

Funciones

Estas son las funciones que realmente implementan ERC-721.

Constructor

@external
def __init__():

En Vyper, como en Python, la función constructora se llama __init__.

    """
    @dev Constructor de contrato.
    """

En Python y en Vyper, también puede crear un comentario especificando una cadena de varias líneas (que comienza y termina con """) y no utilizándola de ninguna manera. Estos comentarios también pueden incluir NatSpec (opens in a new tab).

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

Para acceder a las variables de estado, use self.<nombre de la variable>(de nuevo, igual que en Python).

Funciones de vista

Estas son funciones que no modifican el estado de la cadena de bloques y, por lo tanto, pueden ejecutarse de forma gratuita si se llaman externamente. Si las funciones de vista son llamadas por un contrato, todavía tienen que ejecutarse en cada nodo y, por lo tanto, cuestan gas.

@view
@external

Estas palabras clave que preceden a la definición de una función y que empiezan con una arroba (@) se denominan decoraciones. Especifican las circunstancias en las que se puede llamar a una función.

  • @view especifica que esta función es una vista.
  • @external especifica que esta función en particular puede ser llamada por transacciones y por otros contratos.
def supportsInterface(_interfaceID: bytes32) -> bool:

A diferencia de Python, Vyper es un lenguaje de tipo estático (opens in a new tab). No se puede declarar una variable o un parámetro de función sin identificar el tipo de datos (opens in a new tab). En este caso, el parámetro de entrada es bytes32, un valor de 256 bits (256 bits es el tamaño de palabra nativo de la máquina virtual de Ethereum). La salida es un valor booleano. Por convención, los nombres de los parámetros de las funciones comienzan con un guion bajo (_).

    """
    @dev La identificación de la interfaz se especifica en ERC-165.
    @param _interfaceID Id de la interfaz.
    """
    return self.supportedInterfaces[_interfaceID]

Devuelve el valor del HashMap self.supportedInterfaces, que se establece en el constructor (__init__).

### FUNCIONES DE VISTA ###

Estas son las funciones de vista que hacen que la información sobre los tokens esté disponible para los usuarios y otros contratos.

Esta línea afirma (opens in a new tab) que _owner no es cero. Si lo es, se produce un error y la operación se revierte.

En la máquina virtual de Ethereum (EVM), cualquier almacenamiento que no tenga un valor almacenado en él es cero. Si no hay ningún token en _tokenId, entonces el valor de self.idToOwner[_tokenId] es cero. En ese caso, la función se revierte.

Tenga en cuenta que getApproved puede devolver cero. Si el token es válido, devuelve self.idToApprovals[_tokenId]. Si no hay un aprobador, ese valor es cero.

Esta función comprueba si _operator tiene permiso para gestionar todos los tokens de _owner en este contrato. Debido a que puede haber múltiples operadores, se trata de un HashMap de dos niveles.

Funciones auxiliares de transferencia

Estas funciones implementan operaciones que son parte de la transferencia o la gestión de tókenes.


### FUNCIONES AUXILIARES DE TRANSFERENCIA ###

@view
@internal

Esta decoración, @internal, significa que la función solo es accesible desde otras funciones dentro del mismo contrato. Por convención, los nombres de estas funciones también comienzan con un guion bajo (_).

Hay tres formas en las que se puede permitir que una dirección transfiera un token:

  1. La dirección es la propietaria del token.
  2. La dirección está aprobada para gastar ese token
  3. La dirección es un operador para el propietario del token.

La función anterior puede ser una vista porque no cambia el estado. Para reducir los costes operativos, cualquier función que pueda ser una vista debería ser una vista.

Cuando hay un problema con una transferencia, revertimos la llamada.

Cambie el valor solo si es necesario. Las variables de estado residen en el almacenamiento. Escribir en el almacenamiento es una de las operaciones más caras que realiza la EVM (máquina virtual de Ethereum) (en términos de gas). Por lo tanto, es una buena idea minimizarlo, incluso escribir el valor existente tiene un alto coste.

Tenemos esta función interna porque hay dos maneras de transferir tokens (regular y segura), pero queremos un solo lugar en el código donde lo hagamos para facilitar la auditoría.

Para emitir un evento en Vyper se utiliza una sentencia log (consulte aquí para más detalles (opens in a new tab)).

Funciones de transferencia

Esta función le permite transferir a una dirección arbitraria. A menos que la dirección sea un usuario, o un contrato que sepa cómo transferir tokens, cualquier token que transfiera se quedará atascado en esa dirección y será inútil.

Está bien hacer la transferencia primero porque si hay un problema vamos a revertir de todos modos, así que todo lo que se haga en la llamada será cancelado.

    if _to.is_contract: # comprobar si `_to` es una dirección de contrato

Primero compruebe si la dirección es un contrato (si tiene código). Si no, asuma que es una dirección de usuario y que el usuario podrá usar el token o transferirlo. Pero no deje que le arrulle en una falsa sensación de seguridad. Puede perder tokens, incluso con safeTransferFrom, si los transfiere a una dirección de la que nadie conoce la clave privada.

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

Llame al contrato de destino para ver si puede recibir tókenes ERC-721.

        # Lanza un error si el destino de la transferencia es un contrato que no implementa 'onERC721Received'.
        assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes32)

Si el destino es un contrato, pero uno que no acepta tókenes ERC-721 (o que decidió no aceptar esta transferencia en particular), se revierte.

Por convención, si no quiere tener un aprobador, designe la dirección cero, no a usted mismo.

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

Para establecer una aprobación, puede ser el propietario o un operador autorizado por el propietario.

Acuñar nuevos tokens y destruir los existentes

La cuenta que creó el contrato es el minter, el superusuario que está autorizado a acuñar nuevos NFT. Sin embargo, ni siquiera se le permite quemar los tókenes existentes. Solo el propietario, o una entidad autorizada por el propietario, puede hacerlo.

### FUNCIONES DE ACUÑACIÓN Y QUEMA ###

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

Esta función siempre devuelve True, porque si la operación falla, se revierte.

Solo el acuñador (la cuenta que creó el contrato ERC-721) puede acuñar nuevos tokens. Esto puede ser un problema en el futuro si queremos cambiar la identidad del acuñador. En un contrato de producción, probablemente querrá una función que permita al acuñador transferir los privilegios de acuñador a otra persona.

    # Lanza un error si `_to` es la dirección cero
    assert _to != ZERO_ADDRESS
    # Añadir NFT. Lanza un error si `_tokenId` es propiedad de alguien
    self._addTokenTo(_to, _tokenId)
    log Transfer(ZERO_ADDRESS, _to, _tokenId)
    return True

Por convención, la acuñación de nuevos tokens cuenta como una transferencia desde la dirección cero.

Cualquiera que tenga permiso para transferir un token puede quemarlo. Aunque una quema parece equivalente a una transferencia a la dirección cero, la dirección cero no recibe realmente el token. Esto nos permite liberar todo el almacenamiento que se utilizó para el token, lo que puede reducir el costo de gas de la transacción.

Uso de este contrato

A diferencia de Solidity, Vyper no tiene herencia. Esta es una elección de diseño deliberada para hacer el código más claro y, por lo tanto, más fácil de asegurar. Por lo tanto, para crear su propio contrato ERC-721 de Vyper, tome este contrato (opens in a new tab) y modifíquelo para implementar la lógica de negocio que desee.

Conclusión

A modo de recapitulación, he aquí algunas de las ideas más importantes de este contrato:

  • Para recibir tokens ERC-721 con una transferencia segura, los contratos tienen que implementar la interfaz ERC721Receiver.
  • Incluso si utiliza una transferencia segura, los tokens pueden atascarse si los envía a una dirección cuya clave privada es desconocida.
  • Cuando hay un problema con una operación, es una buena idea revertir la llamada, en lugar de simplemente devolver un valor de fallo.
  • Los tokens ERC-721 existen cuando tienen un propietario.
  • Existen tres maneras de ser autorizados para transferir un NFT. Puede ser el propietario, estar aprobado para un token específico, o ser un operador para todos los tokens del propietario.
  • Los eventos pasados solo son visibles fuera de la cadena de bloques. El código que se ejecuta dentro de la cadena de bloques no puede verlos.

Ahora vaya e implemente contratos seguros de Vyper.

Vea aquí más de mi trabajo (opens in a new tab).

Última actualización de la página: 28 de abril de 2026

¿Te resultó útil este tutorial?