NFT Membership System Development: Smart Contracts and Architecture

In our practice, we've encountered dozens of NFT-membership integrations and seen how erroneous contract architecture leads to privilege leaks. The most common mistake: developers implement a check like `ownerOf(tokenId) == msg.sender` and consider the problem solved. But NFTs can be lent, flash loa

Blockchain Development Services

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1450
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1308
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    1003
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1269
  • image_logo-advance_0.webp
    B2B Advance company logo design
    719
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    1009

In our practice, we've encountered dozens of NFT-membership integrations and seen how erroneous contract architecture leads to privilege leaks. The most common mistake: developers implement a check like ownerOf(tokenId) == msg.sender and consider the problem solved. But NFTs can be lent, flash loaned (for one block), or listed on a marketplace while retaining access through delegation. A proper membership system requires understanding these vectors and explicitly choosing a trust model. Our experience — 5 years in DeFi and NFT, over 20 successful projects — guarantees a reliable solution.

Contract Architecture

Base Model: Token Ownership

For simple use cases (content access, Discord verification), ERC-721 with a balanceOf(user) > 0 check suffices. balanceOf is cheaper than ownerOf for multiple tokens and more robust against edge cases. However, it does not protect against listing: the owner can list the NFT on OpenSea, access gated content, and then cancel the listing.

Tiered Membership via ERC-1155

For multiple access levels (Bronze/Silver/Gold, or month/year/lifetime), ERC-1155 is natively better than ERC-721. Each tokenId represents a tier:

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; // not a member } 

Tiers with cumulative access: Gold includes everything in Silver and Bronze. We check from top to bottom.

Why Use Soulbound Tokens for Membership?

Soulbound tokens (EIP-5192) eliminate the transferability problem: they are permanently tied to the owner's address. If the goal is to tie access to a specific person rather than a wallet, use EIP-5192 or simply override transfer functions:

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. Everything else is forbidden. Problem: losing keys means losing membership. Solution: provide a recovery mechanism via multisig or social recovery (ERC-4337 account abstraction).

How to Implement Time-Based Subscriptions in ERC-721?

Expiring membership requires storing dates. Two approaches:

Approach On-chain timestamp Signature-based off-chain
Storage Mapping tokenId → expiresAt Signed JWT with expiry (on backend)
Gas for check Storage write No writes, cheaper
Trust Full (all on-chain) Requires trust in signing service
Suitable for Fully on-chain systems Web2-hybrid and API integrations

For fully on-chain, use the first approach. ERC-5643 is a draft standard for subscription NFTs with renewSubscription(uint256 tokenId, uint64 duration).

Comparison of Membership Models

Feature ERC-721 ERC-1155 Soulbound (EIP-5192)
Tiers Single type Multiple tiers Single type (if one)
Transfer Yes Yes No
Gas for check balanceOf ~30k balanceOfBatch ~40k balanceOf ~30k
Complexity Low Medium Medium
Application Simple access Tiered membership Personal subscriptions

Integration with Off-Chain Systems

Verification via EIP-1271

For membership verification on the backend without transactions: the user signs a message (EIP-191 or EIP-712), the backend verifies via eth_call to isValidSignature(bytes32 hash, bytes signature) for smart wallets or via ecrecover for EOAs. This is a standard off-chain membership check.

Delegation via delegate.cash

delegate.cash (de facto standard) allows NFT owners to delegate from a cold wallet to a hot wallet. For membership systems, this is important: holders store expensive NFTs in cold wallets and interact via hot wallets. Integration:

IDelegationRegistry constant DELEGATION_REGISTRY = IDelegationRegistry(0x00000000000076A84feF008CDAbe6409d2FE638B); function isMember(address user) public view returns (bool) { if (balanceOf(user) > 0) return true; // Check delegation address[] memory delegators = DELEGATION_REGISTRY.getDelegationsByDelegate(user); for (uint i = 0; i < delegators.length; i++) { if (balanceOf(delegators[i]) > 0) return true; } return false; } 

This is a real need: Moonbirds, Doodles, and other major collections have integrated delegate.cash precisely for this.

Mint Mechanism and Pricing

Allowlist via Merkle Tree is the standard for presale. Gas savings reach 70% compared to storing the list on-chain:

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()); } 

The proof is generated off-chain (merkletreejs), the root is uploaded to the contract. A list of 10,000 addresses results in a proof of ~14 hashes, calldata ~450 bytes.

Example calculation of Merkle proof for 10,000 addresses

A tree of depth 14 has 16,384 leaves. For a proof of 14 hashes (32 bytes each), calldata is 14*32 + 4 (offset) ≈ 452 bytes. At a gas price of 100 gwei, the savings compared to storing the list on-chain is about 0.005 ETH.

What's Included in the Work

  1. Audit of current architecture and requirement specification
  2. Development of smart contracts (Solidity 0.8.x, OpenZeppelin)
  3. Writing unit tests (Foundry/Hardhat) with edge case coverage
  4. Integration with backend (EIP-1271, delegate.cash) and frontend (wagmi, RainbowKit)
  5. Contract documentation (NatSpec) and deployment instructions
  6. Deployment to mainnet/testnet, verification setup on Etherscan
  7. Post-launch support (optional)

Timeline Estimates

ERC-721 membership with tiers and Merkle allowlist — from 2 days. Adding time-based subscriptions (ERC-5643 style) plus backend verification — another 1-2 days. A full system with delegation, soulbound recovery, and frontend — 4-5 days. Contact us for a precise estimate of your project. Order the development of your membership system and get a consultation from our engineer.