A non-transferrable NFT contract with voting capabilities for recognizing elite Farcaster builders.
CrackedCredential is an ERC721Votes token contract that serves as "The self-curated hall of fame for elite Farcaster builders." These credentials are designed to be permanent, non-transferrable recognition tokens that also function as governance voting units.
- Non-transferrable: Once minted, tokens cannot be transferred between addresses (except burning)
- Burnable: Token owners can burn their own tokens to remove them from circulation
- One token per address: Each address can only own one credential token
- Voting Power: Each NFT represents 1 vote in governance decisions
- Auto-delegation: Newly minted tokens are automatically delegated to the recipient
- Delegation: Token holders can delegate their voting power to others without transferring the NFT
- Owner-only minting: Only the contract owner can mint new credentials
- Batch minting: Support for minting multiple tokens in a single transaction
- Approval disabled: Approvals are disabled for security (no
approve
orsetApprovalForAll
) - On-chain metadata: Token metadata is generated and encoded on-chain using Base64
- EIP-712 Support: Secure signature-based delegation using typed structured data
- Zero address protection: Cannot mint to address(0)
- Comprehensive validation: Safer minting with recipient validation
- Token Name: Cracked Farcaster Dev
- Symbol: CFD
- Standard: ERC721Votes (extends ERC721 with OpenZeppelin Votes functionality)
- EIP-712 Domain: "Cracked Farcaster Dev" version "1"
- Clock Mode: Timestamp-based (ERC-6372 compliant)
Deploy the contract with an initial owner address who will have minting privileges:
CrackedCredential credential = new CrackedCredential("Cracked Farcaster Dev", "CFD", <initialOwner>);
Mint single or multiple credentials (auto-delegates to recipients):
// Single mint
credential.safeMint(recipient);
// Batch mint
address[] memory recipients = [addr1, addr2, addr3];
credential.safeMintBatch(recipients);
Minting Requirements:
- Only the contract owner can mint
- Recipient must not be address(0)
- Recipient must not already own a token (one token per address)
- All recipients in batch must be unique
Token owners can burn their own tokens:
// Burn your own token
credential.burn(tokenId);
Burning Requirements:
- Only the token owner can burn their token
- Burning removes the token from circulation and burns the voting power
// Check voting power
uint256 votes = credential.getVotes(account);
// Check who someone delegated to
address delegatee = credential.delegates(account);
// Delegate voting power to someone else (without transferring NFT)
credential.delegate(delegatee);
// Delegate via signature (gasless)
credential.delegateBySig(delegatee, nonce, expiry, v, r, s);
// Check historical voting power
uint256 pastVotes = credential.getPastVotes(account, blockNumber);
- Auto-delegation: When minted, tokens are automatically delegated to the recipient
- NFT Ownership vs Voting Power:
- NFT ownership and voting power are separate
- You keep your NFT even when delegating votes to others
- Transfers are blocked, but delegation is always allowed
- Vote Weight: Each NFT = 1 vote regardless of token ID
- Vote Tracking: Voting power is tracked over time for governance snapshots
- Timestamp-based: Uses
block.timestamp
instead of block numbers for time tracking (ERC-6372)
- Non-transferrable: Tokens cannot be transferred (except burning)
- Approval disabled:
approve()
andsetApprovalForAll()
are disabled - One token per address: Prevents accumulation of multiple tokens
- Zero address protection: Cannot mint to address(0)
- Owner-only minting: Only contract owner can create new credentials
- Burn authorization: Only token owners can burn their own tokens
// Deploy
CrackedCredential cred = new CrackedCredential("Cracked Farcaster Dev", "CFD", <owner>);
// Mint to Alice (auto-delegated)
cred.safeMint(alice);
assert(cred.getVotes(alice) == 1); // Alice has 1 vote
assert(cred.delegates(alice) == alice); // Alice delegated to herself
// Alice delegates to Bob
cred.delegate(bob);
assert(cred.getVotes(alice) == 0); // Alice now has 0 votes
assert(cred.getVotes(bob) == 1); // Bob now has 1 vote
assert(cred.ownerOf(0) == alice); // Alice still owns the NFT
// Alice burns her token
cred.burn(0);
assert(cred.getVotes(bob) == 0); // Bob loses the delegated vote
// In a Governor contract
function propose(...) external returns (uint256) {
// Voting power is automatically checked via ERC721Votes
require(getVotes(msg.sender, block.number - 1) >= proposalThreshold());
// ... rest of proposal logic
}
// These will revert:
credential.safeMint(address(0)); // InvalidRecipient
credential.safeMint(alice); credential.safeMint(alice); // RecipientAlreadyOwnsToken
credential.approve(bob, 0); // ApprovalDisabled
credential.transferFrom(alice, bob, 0); // NotTransferrable
Built with Foundry. Install dependencies:
forge install
Run tests:
forge test
Run with coverage:
forge coverage
The contract follows OpenZeppelin best practices with:
- Comprehensive NatSpec documentation: All functions are fully documented
- Proper function ordering: Functions organized by visibility and purpose
- Error handling: Custom errors for better gas efficiency and clarity
- Security-first design: Disabled approvals, non-transferrable tokens
- Voting integration: Full ERC721Votes implementation for governance
- On-chain metadata: Base64-encoded JSON metadata generated on-chain