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
- Audit of current architecture and requirement specification
- Development of smart contracts (Solidity 0.8.x, OpenZeppelin)
- Writing unit tests (Foundry/Hardhat) with edge case coverage
- Integration with backend (EIP-1271, delegate.cash) and frontend (wagmi, RainbowKit)
- Contract documentation (NatSpec) and deployment instructions
- Deployment to mainnet/testnet, verification setup on Etherscan
- 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.







