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







