Разработка системы временного доступа через NFT

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1Все 1306 услуг
Разработка системы временного доступа через NFT
Средний
~3-5 дней
Часто задаваемые вопросы

Направления блокчейн-разработки

Этапы блокчейн-разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1285
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1198
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    902
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1122
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    589
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    858

Разработка системы временного доступа через NFT

Большинство проектов, которые хотят реализовать временный доступ через NFT, натыкаются на одну и ту же проблему: ERC-721 изначально не содержит понятия «срок действия». Токен либо есть в кошельке, либо его нет. Дата истечения — это дополнительная логика, которую нужно грамотно встраивать, иначе получится либо газовое болото, либо race condition между проверкой и исполнением.

Архитектура временного доступа

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 равным сроку истечения подписки. При попытке доступа к защищённому ресурсу:

  1. Пользователь подписывает сообщение (EIP-4361 Sign-In With Ethereum)
  2. Backend верифицирует подпись, извлекает адрес кошелька
  3. Проверяет кэш Redis → если промах, запрашивает контракт
  4. Если 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 vs. transferable

Выбор между non-transferable (ERC-5192, Soulbound) и transferable доступом — архитектурный, не технический. Soulbound удобен для персонализированных подписок (курсы, лицензии на конкретное лицо). Transferable — для корпоративных лицензий или когда перепродажа доступа — часть модели.

ERC-5192 реализуется просто: locked() возвращает true, все transfer функции revert. OpenZeppelin 5.x добавил ERC721Votes и ERC721Enumerable как extensions — Soulbound реализуется аналогично через _update hook.

Стек и интеграция

Solidity 0.8.20+ с Foundry. ERC-5643 + ERC-5192 (опционально). Off-chain: Node.js/TypeScript, viem для чтения контракта, Redis для кэша доступа, JWT (jose) для сессий. Frontend: wagmi + ConnectKit для wallet connection, react-query для состояния подписки.

Для оплаты в ERC-20 (USDC/DAI) добавляется Permit2 — пользователь подписывает approval и вызов renewSubscription в одной операции, без предварительного approve.

Ориентиры по срокам

Базовый контракт с ERC-5643 + backend middleware проверки доступа + frontend компонент управления подпиской — 3-4 дня. С Permit2 оплатой, мультиуровневым доступом (несколько тарифов) и аналитикой продлений — 5-7 дней.