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.