NFT Access for DAO Members
NFT membership is an alternative to token-weighted governance. Instead of "1 token = 1 vote" it's "1 NFT = 1 vote" or "NFT ownership = access." This solves part of plutocracy problems: large holder doesn't automatically dominate. MolochDAO, Guild.xyz, Friends With Benefits — examples of systems where membership is determined by NFT or whitelist, not just token balance.
Technically the task splits into two: the NFT membership contract itself and access control system verifying NFT ownership for protected operations.
Membership NFT Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
contract DAOMembershipNFT is ERC721, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
// Soulbound: NFT can't be transferred
bool public isSoulbound;
// Merkle root for whitelist minting
bytes32 public merkleRoot;
uint256 private _tokenIdCounter;
uint256 public maxSupply;
// Tier system: 1 = Member, 2 = Core, 3 = Founder
mapping(uint256 => uint8) public memberTier;
mapping(address => bool) public hasMinted;
event MemberAdded(address indexed member, uint256 tokenId, uint8 tier);
event MemberRevoked(uint256 indexed tokenId);
constructor(
string memory name,
string memory symbol,
uint256 _maxSupply,
bool _soulbound
) ERC721(name, symbol) {
maxSupply = _maxSupply;
isSoulbound = _soulbound;
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
// Minting via Merkle proof (for whitelist launch)
function mintWithProof(
bytes32[] calldata proof,
uint8 tier
) external {
require(!hasMinted[msg.sender], "Already minted");
require(_tokenIdCounter < maxSupply, "Max supply reached");
bytes32 leaf = keccak256(abi.encodePacked(msg.sender, tier));
require(MerkleProof.verify(proof, merkleRoot, leaf), "Invalid proof");
hasMinted[msg.sender] = true;
_mintMember(msg.sender, tier);
}
// Minting via governance decision
function mintByGovernance(
address to,
uint8 tier
) external onlyRole(MINTER_ROLE) {
require(!hasMinted[to], "Already has membership");
_mintMember(to, tier);
}
function _mintMember(address to, uint8 tier) internal {
uint256 tokenId = ++_tokenIdCounter;
memberTier[tokenId] = tier;
_safeMint(to, tokenId);
emit MemberAdded(to, tokenId, tier);
}
// Soulbound: block transfers
function _beforeTokenTransfer(
address from, address to, uint256 tokenId, uint256 batchSize
) internal override {
super._beforeTokenTransfer(from, to, tokenId, batchSize);
if (isSoulbound && from != address(0) && to != address(0)) {
revert("Soulbound: non-transferable");
}
}
// Revoke membership via governance (burn)
function revoke(uint256 tokenId) external onlyRole(MINTER_ROLE) {
address owner = ownerOf(tokenId);
hasMinted[owner] = false;
_burn(tokenId);
emit MemberRevoked(tokenId);
}
}
Soulbound vs Transferable NFT
Soulbound (ERC-5192) — NFT can't be sold or transferred. Membership is tied to identity, not capital. Suits DAOs where real participant identity matters (contributor DAO, professional guilds). Downside: if key is lost — no way to transfer membership without governance voting.
Transferable membership NFT — work like collectible access tokens (e.g., Nouns DAO). Can sell DAO seat. More liquid; create market price for membership. Risk: speculative market can distort DAO composition.
Tier System
Simple "yes/no" isn't enough for complex DAOs. Tier levels provide flexibility:
| Tier | Name | Rights | Obtainment |
|---|---|---|---|
| 1 | Observer | Read closed discussions | Whitelist mint |
| 2 | Member | Voting, proposals | Activity 30+ days |
| 3 | Core | Grants committee, veto | Community vote |
| 4 | Founder | Treasury multi-sig | Founding team only |
Tier upgrade via governance proposal: any Tier 2 member can nominate another for Tier 3. Governor votes; if passed, MINTER_ROLE mints upgrade.
On-chain Ownership Verification
Control access to DAO functions via balance check:
contract DAOGovernanceWithNFT {
DAOMembershipNFT public membershipNFT;
modifier onlyMember() {
require(membershipNFT.balanceOf(msg.sender) > 0, "Not a member");
_;
}
modifier onlyTier(uint8 minTier) {
uint256 balance = membershipNFT.balanceOf(msg.sender);
require(balance > 0, "Not a member");
// Find member's highest tier
uint8 highestTier = _getHighestTier(msg.sender);
require(highestTier >= minTier, "Insufficient tier");
_;
}
function createProposal(...) external onlyMember() { ... }
function accessTreasury(...) external onlyTier(3) { ... }
function _getHighestTier(address member) internal view returns (uint8) {
uint256 balance = membershipNFT.balanceOf(member);
uint8 highest = 0;
// For small DAOs (< 1000 members) can iterate
for (uint256 i = 0; i < balance; i++) {
uint256 tokenId = membershipNFT.tokenOfOwnerByIndex(member, i);
uint8 tier = membershipNFT.memberTier(tokenId);
if (tier > highest) highest = tier;
}
return highest;
}
}
For DAOs with thousands of members iteration in on-chain function is problem. Alternative: store address => uint8 tier mapping in contract, updated on mint/burn.
Off-chain Verification via Signature
For access to closed Discord channels, internal sites, or off-chain resources — verify via wallet signature without transaction:
// Frontend: user signs message
const message = `Verify DAO membership\nTimestamp: ${Date.now()}\nAddress: ${address}`;
const signature = await signer.signMessage(message);
// Backend: verify signature + ownership
async function verifyMembership(address: string, signature: string): Promise<boolean> {
// Recover address from signature
const recovered = ethers.verifyMessage(message, signature);
if (recovered.toLowerCase() !== address.toLowerCase()) return false;
// Check NFT balance via RPC
const nft = new ethers.Contract(NFT_ADDRESS, ABI, provider);
const balance = await nft.balanceOf(address);
return balance.gt(0);
}
This pattern is used by Guild.xyz and Collab.Land for Discord gating. User pays no gas, just signs — and gets Discord role if owning NFT.
Development Timeline
Design (3-5 days). Tier structure, soulbound vs transferable, launch mechanics (Merkle whitelist, public mint, governance only), integration with existing Governor or new.
Contract development (1.5-2 weeks). Membership NFT + governance integration + testing.
Off-chain integration (1 week). Backend verification, Discord/Telegram bot via Collab.Land or custom.
Audit (1 week). NFT contracts with access control require audit, especially revoke logic.
Timeline and cost — after detailing requirements.







