Ethereum: de historia a labs

Artículo 17 de 48

35%

completado al llegar aquí

Sistemas distribuidosNota de investigación11 min de lectura

ERC-721 y ERC-1155: propiedad, metadata y safe transfers

ERC-721 y ERC-1155 desde el estado: dueño por id, operadores, el hook onERC721Received, lotes en ERC-1155 y metadata que vive fuera del contrato.

Ruta Ethereum: 0/48

Un juego quiere representar dos cosas: espadas únicas, cada una con su historia, y monedas de oro intercambiables. Con ERC-20 las monedas son fáciles, pero las espadas no, porque ERC-20 solo sabe contar cantidades. Las espadas necesitan una pregunta distinta: no "¿cuánto tiene Ana?", sino "¿quién es el dueño de la espada 7?". De esa pregunta nacen ERC-721 y, después, ERC-1155.

Un NFT (non-fungible token) es un token que no es intercambiable por otro del mismo contrato, porque cada uno tiene un identificador propio. ERC-721 define la interfaz estándar para gestionar, poseer y comerciar esos tokens, cada uno identificado por un uint256 llamado tokenId . Este capítulo lo lee igual que leímos ERC-20: qué estado guarda, quién puede cambiarlo y qué evidencia deja.

Comparación de estándares

ERC-721 o ERC-1155 para un caso concreto

Recomendado

ERC-721

Un contrato por colección; cada tokenId tiene exactamente un dueño.

Ideal para
Objetos únicos donde importa la identidad: títulos, credenciales, piezas de arte.
Ojo con
Mover cien tokens son cien transferencias; transferFrom no comprueba si el receptor puede manejarlos.

ERC-1155

Un contrato con muchos ids; cada id tiene saldos por cuenta, fungibles o no.

Ideal para
Inventarios de juegos y catálogos con muchos tipos y transferencias en lote.
Ojo con
Solo hay aprobación global por operador y todas las transferencias llaman al receptor.
Precaución

Metadata fuera de la cadena

tokenURI o uri apuntan a un JSON que describe el objeto.

Ideal para
Imágenes y atributos que serían caros de guardar en storage.
Ojo con
El token persiste aunque el servidor del JSON cambie o desaparezca.
Ambos registran propiedad; cambian la unidad de cuenta, los permisos y el costo de mover muchos tokens.

Brújula de lectura

Actor, evidencia y estado

Antes de avanzar, ubica quién actúa, qué prueba queda y qué parte del sistema cambia.

Actor

Dueño, aprobado u operador

Tres formas distintas de estar autorizado a mover un token.

Evidencia

Transfer, TransferSingle, TransferBatch

Eventos suficientes para reconstruir propiedad y saldos sin leer storage.

Estado

Dueño por id o saldo por (cuenta, id)

La imagen y los atributos no forman parte de este estado.

Preguntas guía

  • ¿Qué diferencia hay entre aprobar una dirección para un token y aprobar un operador para todos?
  • ¿Por qué existen dos funciones de transferencia en ERC-721?
  • ¿Qué ejecuta exactamente un contrato receptor cuando le llega un NFT?
  • Si el servidor de metadata desaparece, ¿qué sigue siendo verdad en la cadena?

Del saldo al dueño: el estado de ERC-721

En ERC-20 la función central es balanceOf. En ERC-721 es ownerOf(tokenId), que devuelve la dirección dueña y revierte si el token no existe. balanceOf(owner) sigue existiendo, pero cuenta cuántos NFTs tiene una dirección. Por dentro, una implementación típica guarda un mapping de id a dueño y otro de dueño a cantidad.

Los permisos tienen dos niveles. approve(to, tokenId) autoriza a una dirección sobre un token concreto, y esa aprobación se borra en cada transferencia. setApprovalForAll(operator, true) autoriza a un operador sobre todos los tokens presentes y futuros del dueño en ese contrato . El segundo es el que piden los mercados, y es el más delicado: equivale al permiso infinito de ERC-20, pero sobre toda una colección.

Todo contrato ERC-721 debe implementar también ERC-165, un mecanismo para preguntar a un contrato si implementa una interfaz. supportsInterface(0x80ac58cd) devuelve true en un ERC-721 . Un mercado lo usa antes de listar un contrato, y un receptor lo usa para saber qué hooks esperar.

Traza de ejecución: un safeTransferFrom hacia un contrato

ERC-721 tiene dos formas de transferir. transferFrom mueve el token y no pregunta nada al receptor; el propio estándar advierte en mayúsculas que quien llama es responsable de confirmar que el destino puede recibir NFTs, o el token puede perderse para siempre. safeTransferFrom añade una comprobación al final: si el destino tiene código, lo llama.

Sigamos la llamada paso a paso. El token comprueba que quien llama es el dueño, la dirección aprobada para ese id o un operador. Actualiza el dueño, borra la aprobación y emite Transfer. Después mira si to tiene código. Si no lo tiene, termina. Si lo tiene, llama onERC721Received(operator, from, tokenId, data) en el receptor y exige que devuelva 0x150b7a02, el selector de esa función. Cualquier otro valor, o un revert del receptor, revierte toda la transacción.

Figura 1

safeTransferFrom en ERC-721: el hook del receptor decide si la transferencia queda

Fuentes:

[1][5]
safeTransferFrom en ERC-721: el hook del receptor decide si la transferencia queda
Cómo leer esta figura

Diagrama de flujo. safeTransferFrom(from, to, id) entra al paso 1, que pregunta si quien llama es dueño, aprobado u operador; si no, una flecha roja baja a una caja de revert de toda la transacción. Si sí, el paso 2 asigna to como dueño, borra la aprobación y emite Transfer. El paso 3 pregunta si to tiene código: si no, termina con la transferencia aplicada; si sí, el paso 4 hace una llamada externa a onERC721Received, donde corre código del receptor. Si devuelve 0x150b7a02 la transferencia queda aplicada; si devuelve otro valor o revierte, la flecha roja lleva al revert, y el token vuelve a from.

Visual original de TrautsLab. El estado cambia antes de llamar al receptor; si el receptor no devuelve el valor mágico, el revert deshace la transferencia completa.

Este receptor ilustrativo, que compila con OpenZeppelin Contracts 5.x, acepta NFTs de una sola colección y anota quién depositó cada uno:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
 
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
 
contract Boveda721 is IERC721Receiver {
    address public immutable collection;
    mapping(uint256 => address) public depositor;
 
    error WrongCollection(address sender);
 
    constructor(address collection_) {
        collection = collection_;
    }
 
    function onERC721Received(
        address, /* operator */
        address from,
        uint256 tokenId,
        bytes calldata /* data */
    ) external returns (bytes4) {
        if (msg.sender != collection) revert WrongCollection(msg.sender);
        depositor[tokenId] = from;
        return IERC721Receiver.onERC721Received.selector; // 0x150b7a02
    }
}

La comprobación de msg.sender importa porque cualquiera puede llamar onERC721Received directamente con datos inventados. Dentro del hook, msg.sender siempre es el contrato del token, como indica el estándar; el resto de parámetros son lo que ese contrato decidió enviar. Un receptor que confía en from o tokenId sin validar el emisor acepta depósitos que nunca ocurrieron.

ERC-1155: un contrato, muchos ids, transferencias en lote

ERC-1155 parte de una queja práctica: con ERC-20 y ERC-721 cada tipo de token o colección exige su propio contrato. Un juego con miles de objetos desplegaría miles de contratos y pediría miles de aprobaciones. ERC-1155 define un solo contrato donde cada id es un tipo de token configurable, fungible o no . Volviendo al juego: el oro puede ser el id 0 con millones de unidades y cada espada un id con una sola unidad.

El estado pasa a ser balanceOf(owner, id). safeBatchTransferFrom mueve varios ids en una sola llamada y emite TransferBatch. No hay aprobación por token ni por cantidad: solo setApprovalForAll para operadores. Y no existe transferFrom sin comprobación: el estándar solo admite transferencias seguras, de modo que un contrato receptor siempre recibe onERC1155Received (selector 0xf23a6e61) u onERC1155BatchReceived.

La metadata también cambia de forma. ERC-1155 permite una URI con la cadena {id}, que los clientes deben sustituir por el id en hexadecimal, en minúsculas, sin 0x y rellenado a 64 caracteres. Una sola cadena en la cadena describe así miles de tokens. OpenZeppelin añade una observación útil: a diferencia de ERC-20, ERC-1155 no tiene decimals .

Riesgo operativo: metadata mutable y hooks que llaman de vuelta

tokenURI(tokenId) devuelve una URI que puede apuntar a un JSON con nombre, descripción e imagen. El contrato registra quién es dueño del id 7; casi nunca registra cómo es la espada 7. La guía de OpenZeppelin lo dice sin rodeos para ERC-1155: la información del objeto no está en la cadena, así que quien controla el JSON puede cambiar sus atributos y, en un juego, cambiar las reglas . Al evaluar un NFT, pregunta quién puede cambiar la URI y dónde vive el archivo.

Los hooks son llamadas externas, y toda llamada externa cede el control. La documentación de OpenZeppelin advierte que el hook de ERC-721 añade una llamada externa que potencialmente crea una vulnerabilidad de reentrancy . Un contrato que acuña con _safeMint y actualiza su contador después de la llamada puede ser reingresado por el receptor a mitad de la operación. La defensa es la de siempre: escribir el estado antes del hook o usar un guardia de reentrancy.

Por último, setApprovalForAll es el permiso más amplio de ambos estándares y no caduca. Una firma o transacción aprobando a un operador malicioso le entrega la colección entera, incluidos tokens que el usuario reciba después. Revisar y revocar operadores debería ser parte de la higiene de cualquier cartera, igual que los allowances de ERC-20.

Conexiones dentro de la serie

Referencias

Bibliografía académica

Formato IEEENumerado para ingeniería, sistemas y protocolos.
  1. [1]

    W. Entriken, D. Shirley, J. Evans and N. Sachs, "ERC-721: Non-Fungible Token Standard," Ethereum Improvement Proposals, no. 721, Jan. 2018.

    Interfaz ERC721 (ownerOf, safeTransferFrom, approve, setApprovalForAll), ERC721TokenReceiver, extensión de metadata y tokenURI.

  2. [2]

    C. Reitwießner, N. Johnson, F. Vogelsteller, J. Baylina, K. Feldmeier and W. Entriken, "ERC-165: Standard Interface Detection," Ethereum Improvement Proposals, no. 165, Jan. 2018.

    supportsInterface(bytes4) y cálculo de identificadores de interfaz.

  3. [3]

    W. Radomski, A. Cooke, P. Castonguay, J. Therien, E. Binet and R. Sandford, "ERC-1155: Multi Token Standard," Ethereum Improvement Proposals, no. 1155, Jun. 2018.

    balanceOf(owner, id), safeBatchTransferFrom, TransferSingle/TransferBatch, solo transferencias seguras, setApprovalForAll y sustitución {id} en URIs.

  4. [4]

    OpenZeppelin, "ERC-1155," OpenZeppelin Contracts 5.x Documentation.

    Lotes, ausencia de decimals, sustitución de {id}, metadata fuera de la cadena modificable y ERC1155InvalidReceiver.

  5. [5]

    OpenZeppelin, "ERC 721 API Reference," OpenZeppelin Contracts 5.x Documentation.

    safeTransferFrom y _safeMint llaman a onERC721Received; advertencia de llamada externa con riesgo de reentrancy.

Fuentes primarias, papers seminales y especificaciones técnicas citadas en esta investigación.

Rutas vecinas

Si este tema te abrió hambre, sigue por aquí.

Series y labs que comparten área o dependen de esta ruta. Pocas opciones, para no convertir la curiosidad en menú infinito.

Sigue leyendo

Ideas conectadas con este artículo

Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.

Los comentarios se cargan al final de la lectura para no meter peso antes de tiempo.