Inicio / Blockchain / Blockchain: De Cero a Desarrollador / ERC-721: NFTs

ERC-721: NFTs

Tokens no fungibles, metadata, tokenURI, IPFS y ERC-1155.

Intermedio
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

ERC-721: NFTs (Tokens No Fungibles)

Los NFTs (Non-Fungible Tokens) representan activos digitales únicos e indivisibles. El estándar ERC-721 define la interfaz para crear y gestionar estos tokens en Ethereum.

¿Qué es un NFT?

A diferencia de los tokens ERC-20 donde cada unidad es idéntica, cada NFT tiene un identificador único (tokenId) que lo distingue:

ERC-20 (Fungible):
  Mi USDT = Tu USDT (son iguales)

ERC-721 (No Fungible):
  CryptoPunk #3100 ≠ CryptoPunk #7804 (son únicos)

Cada NFT tiene:
- tokenId único dentro de la colección
- Propietario (address)
- Metadata (nombre, imagen, atributos)

La Interfaz ERC-721

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

interface IERC721 {
    // Eventos
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    // Consultas
    function balanceOf(address owner) external view returns (uint256);
    function ownerOf(uint256 tokenId) external view returns (address);

    // Transferencias
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function transferFrom(address from, address to, uint256 tokenId) external;

    // Aprobaciones
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address);
    function setApprovalForAll(address operator, bool approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

Implementación desde Cero

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MiNFT {
    string public name;
    string public symbol;

    mapping(uint256 => address) private _owners;
    mapping(address => uint256) private _balances;
    mapping(uint256 => address) private _tokenApprovals;
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    uint256 private _nextTokenId;

    constructor(string memory _name, string memory _symbol) {
        name = _name;
        symbol = _symbol;
    }

    function balanceOf(address owner) public view returns (uint256) {
        require(owner != address(0), "Zero address");
        return _balances[owner];
    }

    function ownerOf(uint256 tokenId) public view returns (address) {
        address owner = _owners[tokenId];
        require(owner != address(0), "Token no existe");
        return owner;
    }

    function mint(address to) public returns (uint256) {
        uint256 tokenId = _nextTokenId++;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(address(0), to, tokenId);
        return tokenId;
    }

    function transferFrom(address from, address to, uint256 tokenId) public {
        require(ownerOf(tokenId) == from, "Not owner");
        require(to != address(0), "Zero address");
        require(
            msg.sender == from ||
            _tokenApprovals[tokenId] == msg.sender ||
            _operatorApprovals[from][msg.sender],
            "Not authorized"
        );

        _tokenApprovals[tokenId] = address(0);
        _balances[from] -= 1;
        _balances[to] += 1;
        _owners[tokenId] = to;

        emit Transfer(from, to, tokenId);
    }

    function approve(address to, uint256 tokenId) public {
        address owner = ownerOf(tokenId);
        require(msg.sender == owner, "Not owner");
        _tokenApprovals[tokenId] = to;
        emit Approval(owner, to, tokenId);
    }

    function setApprovalForAll(address operator, bool approved) public {
        _operatorApprovals[msg.sender][operator] = approved;
        emit ApprovalForAll(msg.sender, operator, approved);
    }

    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
}

Metadata y TokenURI

Cada NFT puede tener metadata asociada que describe sus atributos:

contract NFTConMetadata is MiNFT {
    mapping(uint256 => string) private _tokenURIs;

    string private _baseURI;

    constructor() MiNFT("Mi Coleccion", "MCOL") {
        _baseURI = "https://api.micoleccion.com/metadata/";
    }

    function tokenURI(uint256 tokenId) public view returns (string memory) {
        require(_owners[tokenId] != address(0), "Token no existe");

        if (bytes(_tokenURIs[tokenId]).length > 0) {
            return _tokenURIs[tokenId];
        }

        // Concatenar baseURI + tokenId
        return string(abi.encodePacked(_baseURI, toString(tokenId), ".json"));
    }
}

Formato de Metadata (JSON)

{
    "name": "Mi NFT #42",
    "description": "Un NFT increible de mi coleccion",
    "image": "ipfs://QmXxx.../42.png",
    "attributes": [
        {
            "trait_type": "Color",
            "value": "Azul"
        },
        {
            "trait_type": "Rareza",
            "value": "Legendario"
        },
        {
            "display_type": "number",
            "trait_type": "Poder",
            "value": 95
        }
    ]
}

Almacenamiento de Metadata

Opción           Pros                    Contras
────────────────────────────────────────────────────
IPFS             Descentralizado,        Necesita pinning
                 inmutable
Arweave          Permanente,             Costo inicial
                 descentralizado
On-chain         100% descentralizado    Muy caro en gas
                 sin dependencias
Servidor propio  Barato, flexible        Centralizado, puede
                                         caerse

IPFS para NFTs

1. Subir imagen a IPFS → CID: QmXyz123...
2. Crear JSON de metadata con el CID de la imagen
3. Subir JSON a IPFS → CID: QmAbc456...
4. Usar ipfs://QmAbc456... como tokenURI

Servicios de pinning:
- Pinata (popular para NFTs)
- Infura IPFS
- NFT.Storage (gratis, para NFTs)

Con OpenZeppelin

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MiColeccionNFT is ERC721, ERC721URIStorage, ERC721Enumerable, Ownable {
    uint256 private _nextTokenId;
    uint256 public maxSupply = 10000;
    uint256 public mintPrice = 0.05 ether;

    constructor()
        ERC721("Mi Coleccion", "MCOL")
        Ownable(msg.sender)
    {}

    function mint(string memory uri) public payable returns (uint256) {
        require(_nextTokenId < maxSupply, "Max supply reached");
        require(msg.value >= mintPrice, "ETH insuficiente");

        uint256 tokenId = _nextTokenId++;
        _safeMint(msg.sender, tokenId);
        _setTokenURI(tokenId, uri);
        return tokenId;
    }

    function withdraw() public onlyOwner {
        payable(owner()).transfer(address(this).balance);
    }

    // Overrides requeridos por Solidity para herencia múltiple
    function tokenURI(uint256 tokenId) public view
        override(ERC721, ERC721URIStorage) returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view
        override(ERC721, ERC721URIStorage, ERC721Enumerable) returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    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);
    }
}

ERC-1155: Multi-Token Standard

ERC-1155 combina fungibles y no fungibles en un solo contrato:

// Un contrato, múltiples tipos de token
contract GameItems is ERC1155 {
    uint256 public constant GOLD = 0;        // Fungible (moneda)
    uint256 public constant SWORD = 1;       // NFT
    uint256 public constant SHIELD = 2;      // NFT
    uint256 public constant POTION = 3;      // Fungible (consumible)

    constructor() ERC1155("https://game.com/api/{id}.json") {
        _mint(msg.sender, GOLD, 1000000, "");
        _mint(msg.sender, SWORD, 1, "");        // Único
        _mint(msg.sender, POTION, 100, "");
    }
}

Resumen

ERC-721 define el estándar para tokens no fungibles en Ethereum. Cada NFT tiene un tokenId único, un propietario y metadata opcional vía tokenURI. La metadata se almacena típicamente en IPFS para descentralización. OpenZeppelin provee implementaciones listas para producción. ERC-1155 permite crear colecciones que combinan tokens fungibles y no fungibles en un solo contrato.

🔒

Ejercicio práctico disponible

Colección NFT básica

Desbloquear ejercicios
// Colección NFT básica
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes