Un ERC721 Enumerable es un ERC721 con funcionalidad añadida que permite a un smart contract listar todos los NFTs que posee una dirección. Este artículo describe cómo funciona ERC721Enumerable y cómo podemos integrarlo en un proyecto ERC721 existente. Usaremos la popular implementación de OpenZeppelin de ERC721Enumerable para nuestra explicación.
Requisitos previos
Dado que ERC721Enumerable es una extensión de ERC721, este artículo asume que el lector ha leído nuestro artículo sobre ERC721 o tiene conocimientos sobre el estándar ERC721.
Swap and Pop
La eliminación de un elemento de una lista en Solidity normalmente se realiza copiando el último elemento en la posición del elemento que se va a eliminar, y luego haciendo un pop en el array (eliminando el último elemento). Desplazar todos los elementos hacia la izquierda es demasiado costoso en términos de gas. La operación para eliminar de una lista se muestra en la animación a continuación, la cual elimina el elemento en el índice 1 (número 5):
¿Por qué ERC721Enumerable?
Para entender por qué necesitamos una extensión como ERC721Enumerable, consideremos un escenario de ejemplo. Si tuviéramos que encontrar todos los NFTs que una wallet posee de un contrato ERC721 particular, ¿cómo lo haríamos con la funcionalidad disponible dentro de ERC721?
Tendríamos que llamar a la función balanceOf() con la dirección del propietario del token, lo que nos daría el número de NFTs que posee esa dirección. Luego, iteraríamos sobre todos los tokenIDs en el contrato ERC721 y llamaríamos a la función ownerOf() para cada uno de estos tokenIDs.
Supongamos que el suministro total de NFTs es 1000 y una dirección posee dos NFTs, el primero y el último. Es decir, posee los tokenIDs #1 y #1000.

Para encontrar los 2 tokenIDs que posee la dirección (token #1 y token #1000), tendríamos que iterar sobre todos los NFTs en un contrato y consultar ownerOf() en ese ID (del 1 al 1000), lo cual es computacionalmente costoso. Además, no siempre conocemos todos los tokenIDs en el contrato, por lo que podríamos no ser capaces de hacer esto.
En las siguientes secciones, aprenderemos cómo ERC721Enumerable resuelve este problema.
Solución ingenua para rastrear la propiedad de tokens
La solución ingenua para rastrear cada token que posee una dirección es almacenar un mapping de la dirección a una lista de NFTs poseídos.
mapping(address owner => uint256[] ownedIDs) public ownedTokens;
Sin embargo, esta solución es ineficiente e incompleta por las siguientes razones:
-
Si el usuario posee muchos tokens, un smart contract que lea su array podría quedarse sin gas al almacenar un array muy largo en memoria.
-
Hay formas más eficientes en cuanto a gas de almacenar una lista de datos (se discuten más adelante).
-
Si queremos eliminar un token particular de la lista de tokens del usuario, necesitamos escanear toda la lista para encontrarlo. Si el array es muy largo, podríamos quedarnos sin gas.
Para resolver los problemas 1 y 2, ERC721 Enumerable usa un array en lugar de un mapping (ver la siguiente sección) y para resolver el tercer problema, se necesita una estructura de datos adicional, la cual mapea el tokenID a su índice correspondiente.
Usando un mapping como un array
Los mappings se pueden usar de manera similar a la de un array, donde las claves son el index y los values son el valor almacenado en ese índice en el array.

Si reemplazamos el array en nuestro ejemplo anterior con un mapping, los indexes del array se convierten en la clave, y los tokenIDs se convierten en los valores.
En Solidity, los mappings son más eficientes en cuanto a gas que los arrays. La longitud de un array se verifica implícitamente siempre que se indexa el array (es decir, para el índice i, verifica si i < array.length). Esta verificación aumenta el costo de gas de usar un array. Al usar un mapping como un array, omitimos esta verificación y, por lo tanto, ahorramos gas.
Sin embargo, a diferencia de los arrays, los mappings no tienen una propiedad length incorporada, que podríamos usar para rastrear el número total de NFTs en un contrato. Por lo tanto, los mappings no siempre son un buen sustituto de los arrays.
En la siguiente sección, profundizaremos en cada estructura de datos de ERC721Enumerable individualmente.
ERC721Enumerable: Las estructuras de datos
ERC721 Enumerable rastrea dos cosas:
- todos los
tokenIDsen existencia. - todos los
tokenIDsque posee una dirección.
Para lograr 1, utiliza las estructuras de datos _allTokens y _allTokensIndex.
Para lograr 2, utiliza las estructuras de datos _ownedTokens y _ownedTokensIndex

Para simplificar, usaremos el mismo conjunto de tokenIDs para cada ejemplo y explicación, es decir, 2, 5, 9, 7 y 1.
_allTokens array:

El array _allTokens nos permite iterar secuencialmente sobre todos los NFTs en un contrato. El array privado _allTokens contiene cada tokenID existente (independientemente de su estado de propiedad).
Inicialmente, el orden de los tokenIDs en _allTokens depende de cuándo fueron acuñados. En el diagrama anterior, el tokenID #2 está en el índice #0 ya que fue acuñado antes que los otros tokenIDs. Este orden puede cambiar tras la quema de tokenIDs.
mapping _allTokensIndex:
El mapping _allTokensIndex, dado un tokenID, devuelve el índice de ese tokenID en el array _allTokens.
En lugar de iterar sobre _allTokens para encontrar el índice de un tokenID, podemos usar el propio tokenID para encontrar su índice en _allTokens usando el mapping _allTokensIndex.
El poder encontrar rápidamente el tokenID permite que la función burn elimine el tokenID de manera eficiente.

El diagrama anterior ilustra un mapping de tokenIDs a sus correspondientes valores de índice. El tokenID #2 se mapea al índice 0 ya que fue el primer token acuñado en el contrato. Este patrón de mapping continúa para cada token que se acuña.
mapping _ownedTokens:
El mapping _ownedTokens se utiliza para rastrear los tokenIDs que posee una dirección. Tiene un mapping anidado (es decir, owner -> index -> tokenID). Mapea cada dirección owner a un index, el cual está dentro del rango del balance de tokens de la dirección. Cada índice se mapea a un tokenID que posee esa dirección.

En el diagrama anterior, la dirección ‘0xAli3c3’ posee 3 NFTs, y por lo tanto tiene un mapping para 3 tokenIDs. La otra dirección (0xb0b) posee un solo token, y por lo tanto tiene un mapping para un solo tokenID. En el índice #2, el mapping anidado para la dirección ‘0xAli3c3’ se mapea al tokenID #1.
mapping _ownedTokensIndex:
Así como _allTokensIndex es la imagen espejo de _allTokens, _ownedTokenIndex es la imagen espejo de _ownedTokens.
_ownedTokensIndex es un mapping de tokenIDs al índice de ese token en _ownedTokens, para ese usuario. Considera el siguiente diagrama:

Si introducimos el tokenID 2 o 9 en _ownedTokensIndex, obtenemos 0 para ambos, porque es el “primer token poseído” tanto para Alice como para Bob.
Además, al igual que _allTokensIndex, el propósito de esta estructura de datos es encontrar un tokenID específico en _ownedTokens para poder eliminarlo de manera eficiente (como cuando el usuario transfiere o quema el token).
Como estas estructuras de datos son privadas, no se puede interactuar directamente con ellas. En la siguiente sección, comprenderemos las funciones que leen y manipulan estas estructuras de datos.
ERC721Enumerable: Funciones
Según la documentación de ERC721, el ERC721Enumerable tiene tres funciones públicas:
totalSupply()

Esta función se utiliza para recuperar el número total de NFTs que existen en un contrato. Devuelve la longitud del array _allTokens.
tokenByIndex()

tokenByIndex es un simple wrapper del array _allTokens, que toma un índice como entrada y devuelve el tokenID almacenado en ese índice en el array _allTokens.
tokenOfOwnerByIndex()

Esta función es un wrapper del mapping _ownedTokens con algunas validaciones de entrada.

En el ejemplo anterior del mapping _ownedTokens, la dirección ‘0xAli3c3’ posee 3 tokenIDs. Si la función es llamada con esta dirección y un index de 2, se devuelve el tokenID #1.
Añadiendo/Eliminando tokenIDs de la Enumeración
Aparte de estas funciones, la implementación de ERC721Enumerable de OpenZeppelin cuenta con 4 funciones privadas adicionales, las cuales son utilizadas por la función _update para asegurar que las estructuras de datos en ERC721Enumerable reflejen la propiedad actual de los tokens.
No entraremos en los detalles de todas estas funciones, ya que no son parte de la especificación de ERC721. Sin embargo, echemos un vistazo a una de ellas:
removeTokenFromOwnerEnumeration()

Esta función se utiliza cuando un tokenID necesita ser eliminado de las estructuras de datos de enumeración de una dirección. Si un propietario vende o quema su NFT, el tokenID de ese NFT necesita ser disociado de la dirección del propietario, aquí es donde _removeTokenFromOwnerEnumeration entra en juego.
El proceso de eliminación
Antes de que se lleve a cabo la eliminación, la función utiliza el mapping _ownedTokensIndex para verificar si el tokenId está en el último índice de los tokenIDs que posee el propietario. Si no está en el último índice, se intercambia (swapped) con el tokenID en el último índice.
Esto es necesario porque si el tokenID se eliminara directamente, quedaría un espacio en los índices de los tokens del propietario, lo que causaría que la función balanceOf() devuelva resultados incorrectos al ser llamada con la dirección del propietario.
Después de este intercambio, la función elimina el tokenID (que ahora es el último tokenID) de _ownedTokensIndex y _ownedTokens, eliminando efectivamente el token de la enumeración.
El resto de este tipo de funciones en la extensión son:
_addTokenToOwnerEnumeration: añade un tokenID a _ownedTokens y _ownedTokensIndex, cada vez que se acuña o transfiere un tokenID a una dirección distinta de cero (non-zero address).
Utiliza la función balanceOf() para determinar el index que se puede asignar al tokenID recién acuñado.
balanceOf() devolverá 3 para una dirección que posee 3 tokenIDs. Esto significa que se puede asignar el índice #3 a un tokenID recién acuñado (ya que la indexación comienza desde 0).

_addTokenToAllTokensEnumeration: añade un tokenID a las estructuras de datos que rastrean todos los NFTs cada vez que se acuña un tokenID, por ejemplo, _allTokensIndex

_removeTokenFromAllTokensEnumeration: se utiliza cuando un tokenID es quemado para mantener las estructuras de datos actualizadas.
_ _removeTokenFromAllTokensEnumeration sigue un proceso de eliminación que es similar a _removeTokenFromOwnerEnumeration.

Armando las piezas: La función _update
Las cuatro funciones privadas de las que aprendimos brevemente en la sección anterior son utilizadas por la función _update para acuñar, quemar o transferir NFTs.

Se invoca cada vez que cambia la propiedad de un tokenID. Hay dos pares de declaraciones condicionales en la función. Vamos a entender lo que hacen:
Declaraciones Condicionales #1: Verificando la Dirección del Remitente
El primer par verifica si el tokenID se está acuñando o transfiriendo. Gestiona la eliminación de un tokenID de las estructuras de datos del propietario anterior. La asignación de un propietario al tokenID se gestiona en la siguiente declaración condicional.
Caso 1: El token es acuñado
Si se está acuñando, llama a _addTokenToAllTokensEnumeration, lo cual añade el tokenID a _allTokens y _allTokensIndex.

Caso 2: El token es transferido
Si se está transfiriendo, se llama a _removeTokenFromOwnerEnumeration, lo cual elimina el tokenID de _ownedTokens y _ownedTokensIndex de la dirección previousOwner que la función toma como entrada.

Declaraciones Condicionales #2: Verificando la Dirección del Receptor
A la primera condición no le interesa la dirección a la que se está transfiriendo el tokenID. Es la segunda declaración condicional la que verifica si el tokenID se está quemando o transfiriendo a una dirección distinta de cero.
Caso 1: El token es quemado
Si se está quemando, se llama a la función _removeTokenFromAllTokensEnumeration, la cual elimina el tokenID de _allTokens y _allTokensIndex.

Caso 2: El token es transferido
Si se está transfiriendo a una dirección distinta de cero, se llama a _addTokenToOwnerEnumeration, lo cual añade el tokenID a _ownedTokens y _ownedTokensIndex de la dirección to.

Añadiendo ERC721Enumerable a tu proyecto
En esta sección, aprenderemos cómo añadir la extensión ERC721Enumerable de OpenZeppelin a nuestro contrato ERC721 en 2 pasos.
1. Importar ERC721Enumerable
En la parte superior de tu archivo ERC721, añade la siguiente línea de código junto con el resto de tus imports:
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
Después de eso, define el contrato de la siguiente manera:
contract YourTokenName is ERC721, ERC721Enumerable{
}
2. Sobrescribir funciones
La inclusión de ERC721Enumerable requiere que algunas funciones de ERC721 sean sobrescritas. Estas funciones son:
function _update(
address to,
uint256 tokenId,
address auth
) internal override(ERC721, ERC721Enumerable) returns (address) {
return super._update(to, tokenId, auth);
}
function _increaseBalance(address account, uint128 value)
internal
override(ERC721, ERC721Enumerable)
{
super._increaseBalance(account, value);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
Nota: Otras extensiones de ERC721 que implementan una función balanceOf() personalizada (por ejemplo, ERC721Consecutive), no pueden ser usadas junto con la extensión ERC721Enumerable ya que interfieren con su funcionalidad.
La enumeración tiene un costo: Advertencias de la extensión ERC721Enumerable
Para cada transferencia, las estructuras de datos en ERC721Enumerable tienen que ser actualizadas. Esto hace que el contrato sea pesado en términos de gas, añadiendo una cantidad considerable de costos de gas. Sin embargo, para los proyectos que deben listar tokenIDs on-chain, este es un gasto necesario.
Autoría
Este artículo fue escrito por Poneta, pasante de investigación en RareSkills.
Aprende más con RareSkills
Revisa nuestro Solidity Bootcamp para aprender conceptos avanzados de Solidity.
Publicado originalmente el 27 de marzo de 2024