Мы регулярно видим проекты, которые пытаются натянуть временный доступ на стандартный ERC-721 и получают головную боль: race condition при проверке срока, перерасход газа из-за неоптимального хранения дат, или полное отсутствие механизма продления. Проблема в том, что ERC-721 не имеет встроенного понятия «истечение». Решение — использовать специализированный стандарт ERC-5643 или построить свою логику на базе mapping. Мы в своей практике предпочитаем первый вариант: он проверен аудитами и сокращает время разработки на 30%. Давайте разберем, как это работает и как избежать типичных ошибок.
Как ERC-5643 решает проблему временного доступа?
В 2022 году появился ERC-5643 — расширение ERC-721 специально для подписочных NFT. Стандарт добавляет два ключевых метода:
interface IERC5643 { event SubscriptionUpdate(uint256 indexed tokenId, uint64 expiration); function renewSubscription(uint256 tokenId, uint64 duration) external payable; function cancelSubscription(uint256 tokenId) external payable; function expiresAt(uint256 tokenId) external view returns (uint64); function isRenewable(uint256 tokenId) external view returns (bool); } expiresAt возвращает unix timestamp истечения подписки для конкретного токена. Хранение — mapping(uint256 => uint64). uint64 достаточно для временных меток на тысячи лет вперёд и занимает один storage slot вместе с другими packed переменными.
Критическая деталь: expiresAt — это view функция, она не блокирует transfer. Если на уровне контракта нужно предотвратить передачу истёкшего токена, нужно переопределить _beforeTokenTransfer в OpenZeppelin ERC-721:
function _beforeTokenTransfer( address from, address to, uint256 tokenId, uint256 batchSize ) internal virtual override { super._beforeTokenTransfer(from, to, tokenId, batchSize); if (from != address(0) && to != address(0)) { // Блокируем transfer истёкших токенов require( block.timestamp < _expirations[tokenId], "Subscription expired" ); } } Альтернатива: разрешить transfer истёкших токенов, но не давать доступ. Это зависит от бизнес-модели — иногда полезно передать токен и возобновить подписку уже новому владельцу.
Off-chain проверка доступа
On-chain состояние — источник истины. Но вызывать expiresAt при каждом HTTP-запросе — медленно. Стандартная архитектура:
Backend middleware читает состояние контракта через multicall при первом обращении, кэширует результат в Redis с TTL равным сроку истечения подписки. При попытке доступа к защищённому ресурсу:
- Пользователь подписывает сообщение (EIP-4361 Sign-In With Ethereum)
- Backend верифицирует подпись, извлекает адрес кошелька
- Проверяет кэш Redis → если промах, запрашивает контракт
- Если
expiresAt(tokenId) > block.timestamp— выдаёт JWT с expiry = min(subscription_expiry, JWT_max_age)
JWT инвалидируется сам по себе когда истекает. Нет необходимости держать blacklist, если JWT TTL выровнен по сроку подписки.
Продление и оплата
renewSubscription принимает duration в секундах и ETH/токен для оплаты. Важный нюанс: продление должно прибавлять к текущему сроку, а не к block.timestamp:
function renewSubscription(uint256 tokenId, uint64 duration) external payable { require(ownerOf(tokenId) == msg.sender, "Not owner"); require(msg.value >= _price * duration / 30 days, "Insufficient payment"); uint64 current = _expirations[tokenId]; // Если подписка уже истекла — продлеваем от текущего момента // Если ещё активна — добавляем к существующему сроку uint64 newExpiry = (current < uint64(block.timestamp)) ? uint64(block.timestamp) + duration : current + duration; _expirations[tokenId] = newExpiry; emit SubscriptionUpdate(tokenId, newExpiry); } Это принципиально для пользователя: если он продлевает активную подписку на месяц, он не теряет оставшиеся дни.
Почему Soulbound (ERC-5192) не всегда подходит?
Выбор между non-transferable (ERC-5192, Soulbound) и transferable доступом — архитектурный, не технический. Soulbound удобен для персонализированных подписок (курсы, лицензии на конкретное лицо). Transferable — для корпоративных лицензий или когда перепродажа доступа — часть модели. ERC-5192 реализуется просто: locked() возвращает true, все transfer функции revert. Однако для временного доступа часто требуется возможность продления, что проще реализовать на ERC-5643.
Стек и интеграция
Solidity 0.8.20+ с Foundry. ERC-5643 + ERC-5192 (опционально). Off-chain: Node.js/TypeScript, viem для чтения контракта, Redis для кэша доступа, JWT (jose) для сессий. Frontend: wagmi + RainbowKit для wallet connection, react-query для состояния подписки.
Для оплаты в ERC-20 (USDC/DAI) добавляется Permit2 — пользователь подписывает approval и вызов renewSubscription в одной операции, без предварительного approve.
| Характеристика | ERC-5643 (рекомендуется) | Кастомное решение |
|---|---|---|
| Газовая эффективность | Высокая (один storage slot) | Средняя (отдельный контракт) |
| Аудирован | Да | Требуется отдельный аудит |
| Время разработки | 2-3 дня | 5-7 дней |
Что входит в разработку системы временного доступа через NFT
- Аудит требований и проектирование архитектуры (on-chain + off-chain)
- Разработка смарт-контракта на Solidity с поддержкой ERC-5643 (или кастомной логики)
- Разработка backend middleware для проверки подписок (Node.js + Redis)
- Интеграция с кошельками (wagmi + RainbowKit)
- Написание документации и тестов (unit + integration)
- Развертывание и поддержка (1 месяц сопровождения)
Ориентиры по срокам
Базовый контракт с ERC-5643 + backend middleware проверки доступа + frontend компонент управления подпиской — 3-4 дня. С Permit2 оплатой, мультиуровневым доступом (несколько тарифов) и аналитикой продлений — 5-7 дней.
Как мы работаем
Наша команда имеет 8+ лет опыта в блокчейн-разработке, выпустила 50+ смарт-контрактов в mainnet, суммарно проаудирована на $5M+ TVL. Мы подходим к каждому проекту с учетом ваших бизнес-задач и предлагаем решение под ключ.
Свяжитесь с нами для обсуждения вашей задачи — мы предложим оптимальное решение под ваш бюджет и сроки. Пишите в Telegram или на почту, получите бесплатную оценку проекта.







