Перейти к содержанию

ERC-721 в Solidity: индивидуальный абонемент

Учебный кредит отвечает на вопрос «сколько единиц принадлежит адресу». Для индивидуального абонемента нам нужен другой вопрос: «кому принадлежит конкретный номер». В ERC-721 каждый существующий tokenId имеет одного текущего владельца. Два абонемента одной коллекции не превращаются в одинаковые доли баланса, хотя могут предоставлять сходную услугу.

Номер абонемента не обязан совпадать с номером курса. В нашем проекте courseId 17 обозначает учебный материал в реестре, а tokenId 1 — первый выданный абонемент. Смешение этих ключей помешало бы выдать два независимых экземпляра права для одного курса. Связь между предметной областью и коллекцией задаётся отдельно.

Коллекция с ограниченным выпуском

Создадим отдельный LibraryPass на OpenZeppelin Contracts 5.4.0. Единственный владелец коллекции может выдавать новые номера. Передачу уже выпущенного абонемента разрешает обычная логика ERC-721. Здесь нет продажи, оплаты, доходности, срока действия или обещания закрытого контента.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract LibraryPass is ERC721, Ownable {
    error IssueLimit();
    uint256 public constant ISSUE_LIMIT = 100;
    uint256 public nextId = 1;
    constructor(address admin)
        ERC721("LibraryPass", "LPASS") Ownable(admin) {}
    function issue(address recipient) external onlyOwner returns (uint256 id) {
        id = nextId;
        if (id > ISSUE_LIMIT) revert IssueLimit();
        nextId = id + 1;
        _safeMint(recipient, id);
    }
}

Это полный исходник учебного контракта. Прямые импорты должны разрешаться из закреплённой зависимости; сейчас установка и компиляция не выполняются. Счётчик начинается с единицы по нашему договору. Стандарт сам по себе не запрещает использовать tokenId ноль, поэтому запрет нельзя приписывать всему ERC-721.

Предел означает максимум сто успешных выдач данной модели. Метод сжигания наружу не предоставлен, повторного использования номера тоже нет. Если позже появится burn, освобождение владения не должно незаметно восстанавливать лимит выдачи. Счётчик исторического выпуска и число существующих токенов — разные показатели.

Разбор одной выдачи

Пусть A является owner коллекции, а R — получателем. Перед вызовом nextId равно 1. A вызывает issue(R). Контракт берёт номер 1, подготавливает следующий номер 2 и выполняет безопасный mint. После ожидаемого успешного выполнения ownerOf(1) возвращает R, а balanceOf(R) увеличивается на единицу.

Запись nextId перед внешней проверкой получателя выбрана намеренно. Безопасный mint может вызвать код контракта R. Он увидит уже подготовленный следующий номер. Если получатель отклонит приём, вся выдача откатится вместе со счётчиком. Вторая попытка получит прежний свободный номер, а не оставит необъяснимый пропуск вследствие отклонённой транзакции.

Один только порядок записи не является универсальной защитой от всех повторных вызовов. Например, если owner сам является получающим контрактом, он может иметь возможность вызвать issue ещё раз. Это полномочие разрешённого администратора, а не автоматическое право любого получателя. Анализ должен учитывать оба свойства: callback и проверку onlyOwner.

Кто вправе выпускать

Передача tokenId 1 от R к Q не делает Q владельцем коллекции. Q получает конкретный абонемент, но не управление issue. Обратное тоже верно: owner коллекции не получает через показанный интерфейс автоматическое право забрать любой чужой tokenId. Административная роль выпуска и токенное владение разделены.

Если A передаст административное владение N, будущие issue разрешены N. Уже существующие владельцы абонементов сохраняются. Если A откажется от владения, выпуск через onlyOwner прекратится, однако обычные передачи токенов останутся доступными их уполномоченным субъектам. Эти последствия следуют из двух независимых частей контракта.

Поэтому панель библиотеки должна использовать разные подписи. «Владелец коллекции» означает административный owner(), «владелец абонемента» — ownerOf(tokenId). Одна общая надпись «владелец» способна показать пользователю ложное представление о доступных действиях, особенно когда оба адреса иногда совпадают.

Неизвестный номер и список абонементов

До выдачи ownerOf(1) отклоняет запрос отсутствующего токена. Нулевой адрес здесь не используется как успешный ответ публичного getter. Такое поведение отличается от незаполненного mapping базового CourseCatalog. Нельзя переносить правило чтения одного контракта на другой по внешне похожему числовому ключу.

balanceOf(R) сообщает количество принадлежащих R токенов. Он не перечисляет их номера. Базовый ERC721 в выбранной версии не включает Enumerable автоматически. Для учебной коллекции можно обнаруживать выдачи и передачи по событиям и затем читать текущее владение. Подключение перечисления является отдельным решением с затратами хранения.

При восстановлении списка учитывайте передачи, а не только события первоначального выпуска. Токен, выданный R, может давно принадлежать Q. Историческая запись mint доказывает прошлый переход, но не отвечает на вопрос о текущем владельце. Официальное состояние на выбранном блоке остаётся решающим для проверки права.

Срок и смысл абонемента

Название LibraryPass не устанавливает срок действия. Если продукту нужен месяц доступа, expiresAt и правило времени должны быть явно добавлены в отдельной модели. Пока существующий токен может передаваться спустя произвольное время. Интерфейс не должен обещать автоматическую годовую подписку, которой код не содержит.

Также tokenId сам по себе не подтверждает личность человека. Адрес может быть контрактом, общим кошельком или аккаунтом, контроль над которым изменился. Приложение связывает адрес с текущей сессией отдельно. Это особенно важно, если библиотека использует абонемент лишь как один фактор разрешения услуги.

Для самостоятельного разбора составьте три сценария: посторонний R пытается issue; A выдаёт контракту, который отклоняет приём; A выдаёт сотый и затем сто первый номер. Объясните ожидаемые изменения nextId, balance и ownerOf. Проверки пока не запускаются. Следующий урок разберёт передачу уже существующего абонемента и область одобрения оператора.

Событие и первая загрузка страницы

При выпуске событие Transfer содержит нулевой from и адрес R в to. Индексатор может использовать эту запись как обнаружение нового номера. Однако интерфейсу полезно затем прочитать ownerOf на выбранном блоке: между обнаружением и показом могла пройти ещё одна передача. Поля события и getter отвечают на разные временные вопросы.

Рассмотрим несколько абонементов одного читателя. A выдаёт R номера 1 и 2, затем R передаёт первый Q. Количество R становится единицей, Q тоже единицей, nextId остаётся 3. Административный owner по-прежнему A. Четыре наблюдения описывают разные стороны одного состояния и позволяют обнаружить ошибку кэша, который уменьшил счётчик выдачи после transfer.

Для упражнений фиксируйте конкретный экземпляр коллекции рядом с tokenId. Номер 1 в другой коллекции является другим объектом. Хранение одного числового ключа без адреса и сети не подходит для постоянных закладок приложения.

Для самостоятельного уточнения: ERC721.sol (5.4.0), eip-721.

Оглавление курса. Листинги, расчёты и наблюдения здесь разобраны как ожидаемое поведение; компиляция, учебные программы и проверки при написании не запускались.

Оглавление серии