Розробка NFT-membership системи
Найчастіша помилка в NFT-membership: розробники реалізують перевірку ownerOf(tokenId) == msg.sender та вважають завдання вирішеним. Але NFT можна одолжити, flash loan-нути (на один блок) або виставити на маркетплейс, зберігши доступ через делегування. Правильна membership система вимагає розуміння цих векторів та явного вибору моделі довіри.
Контрактна архітектура
Базова модель: володіння токеном
Для простих кейсів (доступ до контенту, Discord-верифікація) достатньо ERC-721 з функцією перевірки:
function isMember(address user) public view returns (bool) {
return balanceOf(user) > 0;
}
balanceOf дешевше ownerOf при множественних токенах та стійкіший до edge cases. Але вона не захищає від листингу: власник може виставити NFT на OpenSea, отримати доступ до закритого контенту, та зняти листинг після.
Тиерна membership через ERC-1155
Для кількох рівнів доступу (Bronze/Silver/Gold, або місяц/рік/lifetime) ERC-1155 нативно підходить краще ERC-721. Кожен tokenId — тир:
uint256 public constant TIER_BRONZE = 1;
uint256 public constant TIER_SILVER = 2;
uint256 public constant TIER_GOLD = 3;
function getMemberTier(address user) external view returns (uint256) {
if (balanceOf(user, TIER_GOLD) > 0) return TIER_GOLD;
if (balanceOf(user, TIER_SILVER) > 0) return TIER_SILVER;
if (balanceOf(user, TIER_BRONZE) > 0) return TIER_BRONZE;
return 0; // не учасник
}
Тири з накопленим доступом: Gold включает все що есть в Silver та Bronze. Перевіряємо зверху вниз.
Soulbound (нетрансферабельні) membership токени
Якщо мета — привязать доступ до конкретної людини, а не гаманця, використовуємо EIP-5192 (Minimal Soulbound NFT) або просто переопределяем transfer функції:
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId,
uint256 batchSize
) internal override {
require(from == address(0) || to == address(0), "Soulbound: non-transferable");
super._beforeTokenTransfer(from, to, tokenId, batchSize);
}
from == address(0) — mint, to == address(0) — burn. Все остальное заборонено. Проблема: втрата ключів = втрата membership. Рішення: передбачити recovery механізм через мультисиг або соціальне відновлення (ERC-4337 account abstraction).
Тимчасова membership
Expiring membership вимагає зберігання дат. Два підходи:
On-chain timestamp: маппинг tokenId → expiresAt. Перевірка в isMember() включає block.timestamp < memberships[tokenId].expiresAt. Renewal — транзакція з оплатою, обновляет timestamp. Газ на кожну перевірку.
Signature-based off-chain: бекенд видає підписані JWT з expiry, контракт не зберігає час. Дешевше по газу, але вимагає довіри до сервісу підписів. Підходит для Web2-hybrid систем.
Для fully on-chain — перший підхід. ERC-5643 — черновик стандарту для subscription NFT з renewSubscription(uint256 tokenId, uint64 duration).
Інтеграція з off-chain системами
Верифікація через EIP-1271
Для перевірки membership у бекенді без транзакцій: користувач підписує сообщение (EIP-191 або EIP-712), бекенд верифікує через eth_call до isValidSignature(bytes32 hash, bytes signature) для смарт-кошельків або через ecrecover для EOA.
async function verifyMembership(
userAddress: string,
signature: string,
message: string,
nftContract: ethers.Contract
): Promise<boolean> {
const signerAddress = ethers.verifyMessage(message, signature);
if (signerAddress.toLowerCase() !== userAddress.toLowerCase()) return false;
const balance = await nftContract.balanceOf(userAddress);
return balance.gt(0);
}
Делегування через delegate.xyz
delegate.cash (EIP-нет, але де-факто стандарт) дозволяє власнику NFT делегувати cold wallet → hot wallet. Важливо для membership систем: тримачі зберігають дорогий NFT у cold wallet, взаємодіють через гарячий. Інтеграція:
IDelegationRegistry constant DELEGATION_REGISTRY =
IDelegationRegistry(0x00000000000076A84feF008CDAbe6409d2FE638B);
function isMember(address user) public view returns (bool) {
if (balanceOf(user) > 0) return true;
// Перевіряємо делегування
address[] memory delegators = DELEGATION_REGISTRY.getDelegationsByDelegate(user);
for (uint i = 0; i < delegators.length; i++) {
if (balanceOf(delegators[i]) > 0) return true;
}
return false;
}
Реальна потреба: Moonbirds, Doodles та інші крупні колекції інтегрували delegate.cash саме для цього.
Mint механізм та ціноутворення
Allowlist через Merkle Tree — стандарт для presale:
bytes32 public merkleRoot;
function allowlistMint(bytes32[] calldata proof) external payable {
bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof");
require(msg.value >= PRICE, "Insufficient payment");
_safeMint(msg.sender, _nextTokenId());
}
Proof генерується off-chain (merkletreejs), root завантажується в контракт. Список на 10,000 адрес — proof з ~14 хешей, calldata ~450 байтів.
Орієнтири по строкам
ERC-721 membership з тирами та Merkle allowlist — 2 дні. Додавання тимчасової підписки (ERC-5643 стиль) + бекенд верифікація — ще 1-2 дні. Повна система з делегуванням, soulbound recovery та фронтендом — 4-5 днів.







