Whitelist Management System
A comprehensive Merkle tree-based whitelist system built with IFIF protocol, demonstrating multi-section access control, batch verification, and CSV management capabilities.
Overview
The Whitelist Management System showcases enterprise-grade whitelist functionality using Merkle trees for gas-efficient verification. It provides complete CSV upload handling, real-time verification, and comprehensive analytics for managing large-scale whitelist operations.
Key Features
- Multi-Section Whitelists: Organize addresses into logical sections (VIP, Early Access, General)
- CSV Upload & Processing: Bulk upload with automatic validation and Merkle tree generation
- Batch Verification: Verify multiple addresses simultaneously with gas optimization
- Real-time Analytics: Activity monitoring, verification statistics, and usage patterns
- Administrative Controls: Section management, whitelist updates, and access control
Smart Contract Integration
Whitelist Contract Hooks
The system provides type-safe React hooks for all whitelist operations:
Updating Merkle Roots
import { useUpdateMerkleRoot } from '@/lib/whitelist-contract-hooks'
function UpdateRootButton() {
const { updateMerkleRoot, isLoading, isSuccess, error, txHash, reset } = useUpdateMerkleRoot()
const handleUpdateRoot = async (section: number, newRoot: string) => {
try {
const hash = await updateMerkleRoot({ section, newRoot })
console.log('Transaction hash:', hash)
} catch (err) {
console.error('Failed to update root:', err)
}
}
return (
<Button
onClick={() => handleUpdateRoot(1, '0x123...')}
disabled={isLoading}
>
{isLoading ? 'Updating...' : 'Update Merkle Root'}
</Button>
)
}Address Verification
import { useVerifyAddress } from '@/lib/whitelist-contract-hooks'
function VerifyAddressButton() {
const { data: isVerified, isLoading, refetch } = useVerifyAddress(
1, // section ID
['0xproof1', '0xproof2'], // proof array
'0x123...', // address to verify
true // enabled
)
const handleVerify = () => {
refetch()
}
return (
<div>
<Button onClick={handleVerify} disabled={isLoading}>
{isLoading ? 'Verifying...' : 'Verify Address'}
</Button>
{isVerified !== undefined && (
<div className="mt-2">
<Badge variant={isVerified ? 'default' : 'destructive'}>
{isVerified ? 'Whitelisted' : 'Not Whitelisted'}
</Badge>
</div>
)}
</div>
)
}Batch Verification
import { useVerifyBatch } from '@/lib/whitelist-contract-hooks'
function BatchVerifyButton() {
const proofs = [
['0xproof1a', '0xproof1b'],
['0xproof2a', '0xproof2b']
]
const addresses = ['0x123...', '0x456...']
const { data: batchResults, isLoading, refetch } = useVerifyBatch(
1, // section ID
proofs,
addresses,
true // enabled
)
const handleBatchVerify = () => {
refetch()
}
return (
<div>
<Button onClick={handleBatchVerify} disabled={isLoading}>
{isLoading ? 'Verifying Batch...' : 'Verify Batch'}
</Button>
{batchResults && (
<div className="mt-2">
{batchResults.map((result, index) => (
<div key={index} className="flex justify-between">
<code>{addresses[index]}</code>
<Badge variant={result ? 'default' : 'destructive'}>
{result ? 'Valid' : 'Invalid'}
</Badge>
</div>
))}
</div>
)}
</div>
)
}Section Management
import { useReadMerkleRoot, useSectionExists } from '@/lib/whitelist-contract-hooks'
function SectionInfo({ sectionId }: { sectionId: number }) {
const { data: merkleRoot, isLoading: rootLoading } = useReadMerkleRoot(sectionId)
const { data: exists, isLoading: existsLoading } = useSectionExists(sectionId)
if (rootLoading || existsLoading) return <div>Loading section info...</div>
return (
<Card>
<CardContent>
<h3 className="font-semibold">Section {sectionId}</h3>
<div className="space-y-2">
<p>Status: {exists ? 'Active' : 'Inactive'}</p>
{merkleRoot && (
<p>Root: <code className="text-sm">{merkleRoot}</code></p>
)}
</div>
</CardContent>
</Card>
)
}CSV Upload & Processing
CSV Upload Interface
Complete CSV upload with validation and Merkle tree generation:
import { useCSVUpload } from '@/lib/whitelist-modal-utils'
function CSVUploadForm() {
const {
csvFile,
merkleData,
uploadedAddresses,
generatedRoot,
handleFileUpload,
handleDownloadProofs,
clearUploadedFile
} = useCSVUpload()
return (
<div className="space-y-4">
<div>
<Label htmlFor="csv-file">Upload CSV File</Label>
<Input
id="csv-file"
type="file"
accept=".csv,.txt"
onChange={handleFileUpload}
/>
<p className="text-sm text-muted-foreground mt-1">
Upload a CSV file with Ethereum addresses (one per line)
</p>
</div>
{uploadedAddresses.length > 0 && (
<Card>
<CardContent>
<h3 className="font-semibold">Upload Results</h3>
<div className="space-y-2 mt-2">
<div className="flex justify-between">
<span>Valid Addresses:</span>
<span className="font-mono">{uploadedAddresses.length}</span>
</div>
{generatedRoot && (
<div className="flex justify-between">
<span>Merkle Root:</span>
<code className="text-sm">{generatedRoot}</code>
</div>
)}
</div>
<div className="mt-4 space-x-2">
<Button onClick={handleDownloadProofs} variant="outline">
Download Proofs
</Button>
<Button onClick={clearUploadedFile} variant="outline">
Clear
</Button>
</div>
</CardContent>
</Card>
)}
</div>
)
}Merkle Tree Generation
The system uses standardized Merkle tree generation that matches the contract:
import { buildMerkleTree, MerkleTreeData } from '@/lib/merkle-tree-utils'
// Generate Merkle tree for addresses
function generateWhitelistTree(addresses: string[]): MerkleTreeData {
return buildMerkleTree(addresses)
}
// Example usage
const addresses = ['0x123...', '0x456...', '0x789...']
const treeData = generateWhitelistTree(addresses)
console.log('Merkle Root:', treeData.root)
console.log('Proofs:', treeData.proofs)
// Get proof for specific address
const userProof = treeData.proofs['0x123...']Real-time Verification Interface
Address Verification Form
Interactive verification with proof lookup:
import { useWhitelistVerification } from '@/lib/whitelist-modal-utils'
function AddressVerificationForm({ sectionId }: { sectionId: number }) {
const {
verificationResults,
isVerifying,
verifySingleAddress,
clearResults
} = useWhitelistVerification(sectionId)
const [address, setAddress] = useState('')
const [proof, setProof] = useState('')
const handleVerify = async () => {
const proofArray = proof.split(',').map(p => p.trim()).filter(p => p)
await verifySingleAddress(address, proofArray.join(','), sectionId)
}
return (
<div className="space-y-4">
<div>
<Label htmlFor="address">Ethereum Address</Label>
<Input
id="address"
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder="0x..."
/>
</div>
<div>
<Label htmlFor="proof">Merkle Proof (comma-separated)</Label>
<Input
id="proof"
value={proof}
onChange={(e) => setProof(e.target.value)}
placeholder="0xproof1,0xproof2,0xproof3"
/>
</div>
<Button
onClick={handleVerify}
disabled={!address || !proof || isVerifying}
className="w-full"
>
{isVerifying ? 'Verifying...' : 'Verify Address'}
</Button>
{verificationResults.length > 0 && (
<Card>
<CardContent>
<h3 className="font-semibold">Verification Results</h3>
<div className="space-y-2 mt-2">
{verificationResults.map((result) => (
<div key={result.id} className="flex justify-between items-center">
<code className="text-sm">{result.address}</code>
<Badge variant={result.isValid ? 'default' : 'destructive'}>
{result.isValid ? 'Valid' : 'Invalid'}
</Badge>
</div>
))}
</div>
<Button onClick={clearResults} variant="outline" size="sm" className="mt-2">
Clear Results
</Button>
</CardContent>
</Card>
)}
</div>
)
}Batch Verification Interface
Efficient batch processing for multiple addresses:
import { useWhitelistVerification } from '@/lib/whitelist-modal-utils'
function BatchVerificationForm({ sectionId }: { sectionId: number }) {
const {
verificationResults,
isVerifying,
verifyCSVAddresses,
clearResults
} = useWhitelistVerification(sectionId)
const [csvContent, setCsvContent] = useState('')
const handleBatchVerify = async () => {
await verifyCSVAddresses(csvContent, sectionId)
}
return (
<div className="space-y-4">
<div>
<Label htmlFor="csv-content">CSV Content with Proofs</Label>
<Textarea
id="csv-content"
value={csvContent}
onChange={(e) => setCsvContent(e.target.value)}
placeholder="address,proof1,proof2,proof3,proof4,proof5"
rows={8}
/>
<p className="text-sm text-muted-foreground mt-1">
CSV format: address,proof1,proof2,proof3,proof4,proof5
</p>
</div>
<Button
onClick={handleBatchVerify}
disabled={!csvContent.trim() || isVerifying}
className="w-full"
>
{isVerifying ? 'Verifying Batch...' : 'Verify CSV Addresses'}
</Button>
{verificationResults.length > 0 && (
<Card>
<CardContent>
<h3 className="font-semibold">Batch Verification Results</h3>
<div className="max-h-64 overflow-y-auto mt-2">
{verificationResults.map((result) => (
<div key={result.id} className="flex justify-between items-center py-1">
<code className="text-sm">{result.address}</code>
<Badge variant={result.isValid ? 'default' : 'destructive'}>
{result.isValid ? 'Valid' : 'Invalid'}
</Badge>
</div>
))}
</div>
<div className="mt-2 pt-2 border-t">
<div className="text-sm text-muted-foreground">
{verificationResults.filter(r => r.isValid).length} of {verificationResults.length} addresses verified
</div>
<Button onClick={clearResults} variant="outline" size="sm" className="mt-2">
Clear Results
</Button>
</div>
</CardContent>
</Card>
)}
</div>
)
}Data Layer Integration
Real-time Whitelist Data
Comprehensive data fetching with progressive loading:
import { useCachedWhitelistData } from '@/lib/progressive-whitelist-hooks'
function WhitelistAnalytics() {
const {
sections,
whitelistStats,
isSectionsLoading,
isStatsLoading
} = useCachedWhitelistData()
if (isSectionsLoading || isStatsLoading) {
return <div>Loading analytics data...</div>
}
// whitelistStats is a single object, not an array
const globalStats = whitelistStats
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent>
<div className="text-2xl font-bold">{globalStats?.totalSections || 0}</div>
<p className="text-sm text-muted-foreground">Total Sections</p>
</CardContent>
</Card>
<Card>
<CardContent>
<div className="text-2xl font-bold">{globalStats?.activeSections || 0}</div>
<p className="text-sm text-muted-foreground">Active Sections</p>
</CardContent>
</Card>
<Card>
<CardContent>
<div className="text-2xl font-bold">{globalStats?.totalUpdates || 0}</div>
<p className="text-sm text-muted-foreground">Total Updates</p>
</CardContent>
</Card>
<Card>
<CardContent>
<div className="text-2xl font-bold">{sections?.length || 0}</div>
<p className="text-sm text-muted-foreground">Tracked Sections</p>
</CardContent>
</Card>
</div>
)
}Activity Monitoring
Real-time activity tracking with comprehensive filtering:
import { useCachedWhitelistData } from '@/lib/progressive-whitelist-hooks'
import { useCachedWhitelistData } from '@/lib/progressive-whitelist-hooks'
import { shortenAddress } from '@/config/constants'
function WhitelistActivityTable() {
const {
rootActivities,
adminActivities,
isRootActivitiesLoading,
isAdminActivitiesLoading
} = useCachedWhitelistData()
// Combine activities
const combinedActivities = useMemo(() => {
const combined = []
if (rootActivities) {
rootActivities.forEach(activity => {
combined.push({
id: activity.id,
type: 'root',
section: activity.section,
newValue: activity.newRoot,
oldValue: activity.oldRoot,
initiatedBy: activity.updatedBy,
timestamp: activity.timestamp,
blockNumber: activity.blockNumber
})
})
}
if (adminActivities) {
adminActivities.forEach(activity => {
combined.push({
id: activity.id,
type: 'admin',
activityType: activity.type,
newValue: activity.newValue,
oldValue: activity.oldValue,
initiatedBy: activity.initiatedBy,
timestamp: activity.timestamp,
blockNumber: activity.blockNumber
})
})
}
return combined.sort((a, b) => Number(b.timestamp) - Number(a.timestamp))
}, [rootActivities, adminActivities])
const columns = [
{
accessorKey: 'timestamp',
header: 'Time',
cell: ({ row }) => new Date(Number(row.getValue('timestamp')) * 1000).toLocaleDateString()
},
{
accessorKey: 'type',
header: 'Type',
cell: ({ row }) => {
const type = row.getValue('type')
return (
<Badge variant={type === 'root' ? 'default' : 'secondary'}>
{type === 'root' ? 'Root Updated' : 'Admin Action'}
</Badge>
)
}
},
{
accessorKey: 'section',
header: 'Section',
cell: ({ row }) => {
const section = row.getValue('section')
return section !== undefined ? (
<span className="font-mono">{section}</span>
) : '-'
}
},
{
accessorKey: 'initiatedBy',
header: 'Initiated By',
cell: ({ row }) => (
<code className="text-sm">{shortenAddress(row.getValue('initiatedBy'))}</code>
)
},
{
accessorKey: 'blockNumber',
header: 'Block',
cell: ({ row }) => (
<span className="font-mono">{Number(row.getValue('blockNumber')).toLocaleString()}</span>
)
}
]
return (
<DataTable
data={combinedActivities}
columns={columns}
loading={isRootActivitiesLoading || isAdminActivitiesLoading}
/>
)
}Error Handling & Validation
Comprehensive error handling for whitelist operations:
function WhitelistErrorHandler() {
const handleWhitelistError = (error: Error, context: string) => {
const message = error.message.toLowerCase()
if (message.includes('insufficient permissions')) {
toast.error('Access Denied', {
description: 'You do not have permission to perform this action'
})
} else if (message.includes('zero hash')) {
toast.error('Invalid Merkle Root', {
description: 'Merkle root cannot be zero hash'
})
} else if (message.includes('user rejected')) {
toast.error('Transaction Cancelled', {
description: 'Transaction was cancelled by user'
})
} else {
toast.error(`${context} Failed`, {
description: error.message
})
}
}
return { handleWhitelistError }
}
// Input validation utilities
export function validateEthereumAddress(address: string): boolean {
return /^0x[a-fA-F0-9]{40}$/.test(address)
}
export function validateMerkleRoot(root: string): boolean {
return /^0x[a-fA-F0-9]{64}$/.test(root) && root !== '0x0000000000000000000000000000000000000000000000000000000000000000'
}Contract Functions
Core Whitelist Functions
The Whitelist contract provides these core functions:
// Update Merkle root for a section (Manager only)
function updateRoot(uint256 section, bytes32 newRoot) public onlyManager
// Verify a single address
function verifyAddress(uint256 section, bytes32[] calldata proof, address account)
public view returns (bool)
// Verify multiple addresses in batch
function verifyBatch(uint256 section, bytes32[][] calldata proofs, address[] calldata accounts)
public view returns (bool[] memory)
// Check if section exists
function sectionExists(uint256 section) public view returns (bool)
// Generate leaf hash for address
function generateLeaf(address account) public pure returns (bytes32)Role Integration
The Whitelist contract integrates with the Roles contract:
import { useReadRoleHelper } from '@/lib/whitelist-contract-hooks'
function RoleHelperInfo() {
const { data: roleHelper, isLoading } = useReadRoleHelper()
return (
<div>
<h3>Role Helper Contract</h3>
{isLoading ? (
<div>Loading...</div>
) : (
<code className="text-sm">{roleHelper}</code>
)}
</div>
)
}Security Considerations
- Merkle Proof Validation: Always verify proofs on-chain before granting access
- Input Sanitization: Validate all Ethereum addresses before processing
- Access Control: Implement proper role-based permissions for whitelist management
- Zero Hash Prevention: Contract prevents zero hash merkle roots
- Data Privacy: Only store necessary information on-chain
Performance Optimization
- Progressive Loading: Uses progressive data loading for large datasets
- Batch Operations: Use batch verification for multiple addresses
- Data Caching: React Query caches data with background updates
- Database Indexing: Optimized Ponder schema for common queries
- Component Optimization: Proper memoization for expensive operations
The Whitelist Management System provides a complete enterprise-grade solution for managing large-scale whitelists with gas-efficient verification and comprehensive administrative tools.