ERC-721 и NFT

ERC-721 — стандарт уникальных токенов: у каждого свой id и владелец. Это технология за NFT.
Если ERC-20 — это рубли (любая купюра равна другой), то ERC-721 — это билеты с местами: каждый уникален и принадлежит ровно одному.

ERC-721 описывает невзаимозаменяемые (non-fungible) токены. Каждый токен — это tokenId (уникальное число) со своим владельцем. Ключевые функции: ownerOf(tokenId) (кто владеет), balanceOf(owner) (сколько NFT у адреса), transferFrom/safeTransferFrom (передать конкретный токен), approve/setApprovalForAll (разрешения), и tokenURI(tokenId) — ссылка на метаданные (картинку, имя, атрибуты).

   ERC-20  vs  ERC-721
   ===================
   ERC-20:  balanceOf[addr] = 100   (взаимозаменяемо, сумма)
   ERC-721: ownerOf[1] = alice
            ownerOf[2] = bob        (каждый id уникален,
            ownerOf[3] = alice       у одного владельца)
            tokenURI[1] -> ipfs://.../1.json (метаданные)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

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

contract GameItem is ERC721, Ownable {
    uint256 private _nextId;

    constructor() ERC721("GameItem", "ITM") Ownable(msg.sender) {}

    // только владелец контракта может чеканить новые NFT
    function mint(address to) external onlyOwner returns (uint256 id) {
        id = _nextId++;
        _safeMint(to, id); // создаёт токен id и присваивает владельцу
    }
}

Как работает под капотом (EVM/газ)

Внутри ERC-721 — маппинги tokenId => owner и owner => balance. Минт записывает владельца нового id и излучает Transfer от нулевого адреса (это и есть «рождение» токена). safeTransferFrom проверяет, что получатель-контракт умеет принимать NFT (реализует onERC721Received) — иначе перевод откатится, чтобы токен не «застрял». Метаданные (tokenURI) обычно не лежат в storage целиком: там хранят лишь ссылку (часто на IPFS), а саму картинку — вне сети, потому что хранить байты изображения on-chain неподъёмно дорого.

# Та же логика на Python: ядро ERC-721
owner_of = {}            # tokenId -> owner
balance_of = {}          # owner -> count
token_uri = {}           # tokenId -> ссылка на метаданные
next_id = 0

def mint(to, uri):
    global next_id
    tid = next_id; next_id += 1
    owner_of[tid] = to
    balance_of[to] = balance_of.get(to, 0) + 1
    token_uri[tid] = uri
    print(f"Mint #{tid} -> {to}")
    return tid

def transfer(frm, to, tid):
    assert owner_of[tid] == frm, "REVERT: not owner"
    owner_of[tid] = to
    balance_of[frm] -= 1
    balance_of[to] = balance_of.get(to, 0) + 1

t = mint("alice", "ipfs://meta/0.json")
transfer("alice", "bob", t)
print("ownerOf #0:", owner_of[0], "| baalice:", balance_of.get("alice", 0))
print("tokenURI #0:", token_uri[0])

«Та же логика на Python ▶». Каждый id уникален и принадлежит одному владельцу; перевод меняет владельца конкретного токена, а не «сумму», как в ERC-20.

Частые ошибки

  • Хранить полную картинку on-chain — это запредельно дорого; хранят ссылку на IPFS/Arweave.
  • Использовать transferFrom вместо safeTransferFrom при отправке на контракт — токен может «застрять» у получателя, который не умеет его принять.
  • Минтить без контроля доступа — кто угодно начеканит себе NFT.

Best practices

  • Наследуйте проверенный ERC721 от OpenZeppelin, а не пишите стандарт вручную.
  • Для пользовательских адресов и контрактов используйте safeTransferFrom.
  • Метаданные кладите на децентрализованное хранилище (IPFS) и фиксируйте контент-хэшем, чтобы их нельзя было подменить.

approve, setApprovalForAll и маркетплейсы

Разрешения в ERC-721 работают похоже на ERC-20, но точечнее. approve(spender, tokenId) разрешает адресу распоряжаться одним конкретным токеном, а setApprovalForAll(operator, true) — сразу всеми вашими токенами этой коллекции. Именно второй вызов делают маркетплейсы вроде OpenSea: вы один раз разрешаете контракту площадки оперировать вашей коллекцией, и дальше он может перевести проданный NFT покупателю без отдельного approve на каждую сделку. Это удобно, но и рискованно: дав setApprovalForAll вредоносному или взломанному контракту, вы рискуете всей коллекцией сразу. Поэтому действует то же правило, что и с «бесконечным» allowance в ERC-20 — выдавайте операторские права только проверенным площадкам и периодически отзывайте старые разрешения. Расширение ERC721Enumerable добавляет перебор токенов, но дорого по газу, поэтому в продакшене перечисление чаще строят через события и индексатор.

Итоги

ERC-721 даёт уникальные токены с владельцем и метаданными; это фундамент NFT. От ERC-20 он отличается тем, что оперирует tokenId, а не суммой. Дальше — самый важный раздел: безопасность смарт-контрактов.

Проверьте себя
1. Чем ERC-721 принципиально отличается от ERC-20?
AНичем, это одно и то же
BERC-721 оперирует уникальными tokenId с владельцем, а не взаимозаменяемой суммой
CERC-721 нельзя передавать
DERC-721 не требует газа
2. Зачем safeTransferFrom вместо обычного transferFrom при отправке NFT на контракт?
AОн дешевле
BОн проверяет, что контракт-получатель умеет принимать NFT, иначе откатывается
CОн шифрует токен
DОн не требует владельца