Skip to content

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.