Skip to content

IFIF V2 Enhanced Features

Comprehensive guide to implementing IFIF V2 enhanced features in your UI, including vesting schedules, distributor configurations, automatic stage transitions, and nonce-based signature security.

Overview

IFIF V2 introduces powerful enhancements to the base IFIF protocol:

  • Automatic Stage Transitions: No manual stage progression required
  • Enhanced Purchase Controls: Soft/hard caps for better crowd management
  • Vesting Systems: Graduated token claims and fund withdrawals
  • Multi-Distributor Support: Per-distributor fee configurations
  • Nonce-Based Security: Replay attack prevention for all signatures
  • Sweep Mechanism: Unclaimed asset recovery after expiration

Version Detection

Project Version Detection

Use the version detection hook to identify V2-capable projects:

import { useProjectVersion } from '@/lib/use-project-version'
 
function ProjectInterface({ projectAddress }: { projectAddress: Address }) {
  const { version, isV2, isLoading } = useProjectVersion(projectAddress)
 
  if (isLoading) return <div>Detecting version...</div>
 
  return (
    <div>
      <Badge variant={isV2 ? 'default' : 'secondary'}>
        Version {version}
      </Badge>
      {isV2 && (
        <div className="text-sm text-green-600">
          ✓ Enhanced V2 features available
        </div>
      )}
    </div>
  )
}

Factory Version Detection

Detect the factory implementation version for deployment:

import { useFactoryImplementationVersion } from '@/lib/use-implementation-version'
 
function DeploymentInterface() {
  const { version, isV2, implementationAddress } = useFactoryImplementationVersion()
 
  return (
    <Alert className={isV2 ? 'border-green-200' : 'border-gray-200'}>
      <AlertDescription>
        Factory Version: {version}
        {isV2 && <span className="ml-2 text-green-600">✓ V2 Deployment Available</span>}
      </AlertDescription>
    </Alert>
  )
}

V2 Initialization

Initialize V2 Features

After deploying a V2-compatible project, initialize enhanced features:

import { InitializeV2Modal } from '@/components/ifif-v2-project-client-modals'
import { useSignatureHelper } from '@/lib/signature-store'
 
function ProjectSetup({ 
  projectAddress, 
  projectId 
}: { 
  projectAddress: Address
  projectId: number 
}) {
  const [isInitModalOpen, setIsInitModalOpen] = useState(false)
  const { openModal: openSignatureModal } = useSignatureHelper()
 
  return (
    <>
      <Button onClick={() => setIsInitModalOpen(true)}>
        Initialize V2 Features
      </Button>
 
      <InitializeV2Modal
        isOpen={isInitModalOpen}
        onClose={() => setIsInitModalOpen(false)}
        projectAddress={projectAddress}
        projectId={projectId}
        onSuccess={() => {
          console.log('V2 features initialized')
          setIsInitModalOpen(false)
        }}
      />
    </>
  )
}

V2 Configuration Structure

The V2 initialization requires these enhanced parameters:

interface Config_v2 {
  privateSaleStartEpoch: number    // When private sale automatically starts
  purchaseHardCapPercent: number   // Max purchase as % of funding goal (0-100)
  purchaseSoftCap: bigint          // Minimum purchase amount
  minNFTWeight: bigint             // Minimum weight for NFT operations
}

Nonce Management

Understanding Nonces

IFIF V2 uses nonces to prevent signature replay attacks. Each signature type tracks nonces separately:

  • NONCE_NEXT_CONFIG (0): For NextConfig updates
  • NONCE_FUND_CLAIM_CONFIG (1): For FundClaimConfig updates
  • NONCE_DISTRIBUTOR_CONFIG (2): For DistributorConfig updates

Fetching Current Nonces

Use the useNonce hook to fetch current nonces from the contract:

import { useNonce } from '@/lib/use-nonce'
import { SignatureType } from '@/lib/signature-store'
 
function ConfigurationForm({ 
  projectAddress,
  signerAddress 
}: {
  projectAddress: Address
  signerAddress: Address
}) {
  const { nonce, isLoading, error } = useNonce(
    projectAddress,
    SignatureType.NONCE_NEXT_CONFIG,
    signerAddress
  )
 
  if (isLoading) return <div>Loading nonce...</div>
  if (error) return <div>Error loading nonce</div>
 
  return (
    <div>
      <Label>Current Nonce</Label>
      <Input value={nonce?.toString() || '0'} disabled />
      <p className="text-xs text-muted-foreground">
        Next valid nonce: {(nonce || 0n) + 1n}
      </p>
    </div>
  )
}

Nonce Validation in Forms

Modal components automatically validate nonces:

<form.Field 
  name="nonce"
  validators={{
    onChange: ({ value }) => {
      const num = BigInt(value || '0')
      const minNonce = contractNonce || 0n
      return num < minNonce
        ? `Nonce cannot be lower than current nonce (${minNonce})`
        : undefined
    },
  }}
>
  {(field) => (
    <div>
      <Label>Signature Nonce</Label>
      <Input
        type="number"
        min={contractNonce?.toString() || '0'}
        value={String(field.state.value || '0')}
        onChange={(e) => field.handleChange(e.target.value)}
      />
      <FieldInfo field={field} />
    </div>
  )}
</form.Field>

Distributor Management

Adding Distributor Configurations

V2 allows multiple distributors with individual fee structures:

import { UpdateDistributorConfigModal } from '@/components/ifif-v2-project-client-modals'
 
function DistributorManagement({ 
  projectAddress,
  projectId,
  projectOwner 
}: {
  projectAddress: Address
  projectId: number
  projectOwner: Address
}) {
  const [isModalOpen, setIsModalOpen] = useState(false)
 
  return (
    <>
      <Button onClick={() => setIsModalOpen(true)}>
        Add Distributor
      </Button>
 
      <UpdateDistributorConfigModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        mode="new"
        projectAddress={projectAddress}
        projectId={projectId}
        projectOwner={projectOwner}
        projectName="My Project" // Optional: for display
        onSuccess={() => {
          console.log('Distributor added')
          setIsModalOpen(false)
        }}
      />
    </>
  )
}

Note: The projectOwner prop is required for authorization validation.

Distributor Configuration Structure

interface DistributorConfig {
  distributorFeePercent: number    // Distributor's fee (0-100)
  projectOwnerFeePercent: number   // Project owner's fee (0-100)
  platformFeePercent: number       // Platform fee (0-100)
  nonce: bigint                    // Replay protection nonce
}

Updating Existing Distributors

function UpdateDistributorFees({
  projectAddress,
  projectOwner,
  distributorConfig
}: {
  projectAddress: Address
  projectOwner: Address
  distributorConfig: DistributorConfig
}) {
  const [isModalOpen, setIsModalOpen] = useState(false)
 
  return (
    <>
      <Button variant="outline" onClick={() => setIsModalOpen(true)}>
        Update Fees
      </Button>
 
      <UpdateDistributorConfigModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        mode="update"
        projectAddress={projectAddress}
        projectOwner={projectOwner}
        distributorConfig={distributorConfig}
        onSuccess={() => setIsModalOpen(false)}
      />
    </>
  )
}

Fund Claim Vesting

Configuring Fund Vesting

Project owners can configure graduated fund withdrawals. This requires a 2-signature authorization (Manager + Project Owner):

import { UpdateFundClaimConfigModal } from '@/components/ifif-v2-project-client-modals'
 
function FundVestingSetup({
  projectAddress,
  projectId,
  projectOwner
}: {
  projectAddress: Address
  projectId: number
  projectOwner: Address
}) {
  const [isModalOpen, setIsModalOpen] = useState(false)
 
  return (
    <>
      <Button onClick={() => setIsModalOpen(true)}>
        Configure Fund Vesting
      </Button>
 
      <UpdateFundClaimConfigModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        projectAddress={projectAddress}
        projectId={projectId}
        projectOwner={projectOwner}
        projectName="My Project" // Optional: for display
        onSuccess={() => setIsModalOpen(false)}
      />
    </>
  )
}
Multi-Signature Process:
  1. Manager generates and signs the FundClaimConfig
  2. Manager signature is provided to the modal (via form field or signature helper)
  3. Project Owner approves by submitting the transaction
  4. Both signatures validate the configuration update

Fund Claim Configuration Structure

// Data structure from Ponder/API (read operations)
interface FundClaimConfig {
  startEpoch: string      // When vesting begins (Unix timestamp)
  endEpoch: string        // When vesting completes (Unix timestamp)
  periodLength: string    // Claim interval in seconds (min 86400 = 1 day)
}
 
// Note: For write operations (signatures/transactions), parameters are:
// - startEpoch: number
// - endEpoch: number  
// - periodLength: number
// - nonce: bigint (for multi-signature authorization)

Claiming Vested Funds

import { ClaimFundModal } from '@/components/ifif-v2-project-client-modals'
 
function FundClaimInterface({
  projectAddress,
  projectId,
  projectOwner,
  projectStage
}: {
  projectAddress: Address
  projectId: number
  projectOwner: Address
  projectStage?: number
}) {
  const [isModalOpen, setIsModalOpen] = useState(false)
 
  return (
    <>
      <Button onClick={() => setIsModalOpen(true)}>
        Claim Vested Funds
      </Button>
 
      <ClaimFundModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        projectAddress={projectAddress}
        projectId={projectId}
        projectOwner={projectOwner}
        projectStage={projectStage}
        projectName="My Project" // Optional: for display
        onSuccess={() => setIsModalOpen(false)}
      />
    </>
  )
}

Note: The modal automatically fetches fundClaimConfig and calculates vesting status internally using the V2 configuration hooks.

Investor Claim Vesting

Configuring Investor Vesting

Factory managers only (requires MANAGER_ROLE) can configure graduated token claims for investors. This is a factory-level operation that affects investor token distribution.

import { UpdateClaimConfigModal } from '@/components/ifif-v2-project-client-modals'
 
function InvestorVestingSetup({
  projectAddress,
  projectId
}: {
  projectAddress: Address
  projectId: number
}) {
  const [isModalOpen, setIsModalOpen] = useState(false)
 
  return (
    <>
      <Button onClick={() => setIsModalOpen(true)}>
        Configure Investor Vesting
      </Button>
 
      <UpdateClaimConfigModal
        isOpen={isModalOpen}
        onClose={() => setIsModalOpen(false)}
        projectAddress={projectAddress}
        projectId={projectId}
        projectName="My Project" // Optional: for display
        onSuccess={() => setIsModalOpen(false)}
      />
    </>
  )
}

Claim Configuration Structure

// ClaimConfig parameters (factory-level investor vesting)
interface ClaimConfigParams {
  startEpoch: number      // When investor claiming starts (Unix timestamp)
  periodLength: number    // Vesting period length in seconds (min 86400 = 1 day)
}
 
// Note: Unlike FundClaimConfig, ClaimConfig does NOT require endEpoch
// The vesting continues indefinitely based on periodLength intervals
Important Distinctions:
  • ClaimConfig: For investor token claims (factory operation, requires Manager role)
  • FundClaimConfig: For project owner fund withdrawals (project operation, requires Project Owner)
  • ClaimConfig uses startEpoch + periodLength only
  • FundClaimConfig uses startEpoch + endEpoch + periodLength

Progressive Data Loading

Loading V2 Configurations

Use progressive hooks to load V2-specific data efficiently:

import { useCachedIFIFV2Data } from '@/lib/progressive-ifif-v2-hooks'
 
function V2DataDisplay() {
  const {
    projectV2Configs,
    fundClaimConfigs,
    distributorConfigs,
    claimConfigs,
    fundClaims,
    sweeps,
    isLoading,
    loadMore
  } = useCachedIFIFV2Data()
 
  if (isLoading) return <LoadingSpinner />
 
  return (
    <div className="space-y-6">
      <section>
        <h3>V2 Projects ({projectV2Configs.length})</h3>
        {projectV2Configs.map(config => (
          <ProjectV2Card key={config.id} config={config} />
        ))}
      </section>
 
      <section>
        <h3>Distributor Configs ({distributorConfigs.length})</h3>
        {distributorConfigs.map(config => (
          <DistributorCard key={config.id} config={config} />
        ))}
      </section>
 
      {isLoading && (
        <Button onClick={loadMore} disabled={isLoading}>
          Load More
        </Button>
      )}
    </div>
  )
}

Project V2 Configuration Hook

Get comprehensive V2 status for a project:

import { useProjectV2Configuration } from '@/lib/progressive-ifif-v2-configuration-hooks'
 
function ProjectV2Status({ projectId }: { projectId: number }) {
  const v2Config = useProjectV2Configuration(projectId)
 
  return (
    <div className="space-y-4">
      <Alert className={v2Config.status.hasConfig ? 'border-green-200' : 'border-gray-200'}>
        <AlertDescription>
          {v2Config.status.hasConfig ? (
            <span className="text-green-600">✓ V2 Initialized</span>
          ) : (
            <span className="text-gray-600">V2 Not Initialized</span>
          )}
        </AlertDescription>
      </Alert>
 
      {v2Config.config && (
        <div className="text-sm space-y-2">
          <div>Private Sale Start: {new Date(v2Config.config.privateSaleStartEpoch * 1000).toLocaleString()}</div>
          <div>Hard Cap: {v2Config.config.purchaseHardCapPercent}%</div>
          <div>Soft Cap: {formatEther(v2Config.config.purchaseSoftCap)} tokens</div>
        </div>
      )}
 
      {v2Config.fundClaimConfig && (
        <Alert className="border-blue-200">
          <AlertDescription>
            <strong>Fund Vesting Configured</strong>
            <div className="text-sm mt-1">
              Period: {v2Config.fundClaimConfig.periodLength / 86400} days
            </div>
          </AlertDescription>
        </Alert>
      )}
 
      {!v2Config.status.canUpdateFundClaimConfig && (
        <Alert className="border-red-200">
          <AlertDescription>
            {v2Config.status.fundClaimConfigUpdateError}
          </AlertDescription>
        </Alert>
      )}
    </div>
  )
}

Signature Management

V2 Signature Types

IFIF V2 adds three new signature structures. Note the distinction between signature data (used in EIP-712 signing) and runtime data (from Ponder/API):

// ConfigV2 - For V2 initialization (signature structure)
interface ConfigV2Signature {
  privateSaleStartEpoch: bigint | string  // Form uses string, contract uses bigint
  purchaseHardCapPercent: bigint | string
  purchaseSoftCap: bigint | string
  minNFTWeight: bigint | string
}
 
// FundClaimConfig - For project owner vesting (signature structure)
interface FundClaimConfigSignature {
  startEpoch: bigint | number    // Contract parameter
  endEpoch: bigint | number      // Contract parameter
  periodLength: bigint | number  // Contract parameter
  nonce: bigint                  // Replay protection (separate from config)
}
 
// DistributorConfig - For distributor fees (signature structure)
interface DistributorConfigSignature {
  distributorFeePercent: bigint | number
  projectOwnerFeePercent: bigint | number
  platformFeePercent: bigint | number
  nonce: bigint                  // Replay protection (separate from config)
}
 
// Runtime data from Ponder/API uses string types:
interface FundClaimConfigData {
  startEpoch: string
  endEpoch: string
  periodLength: string
  // Note: nonce is not stored in the config data itself
}
Type Conversion Pattern:
  • Forms use string for user input
  • Contracts use bigint for precision
  • Ponder returns string for BigInt values
  • Always convert between types appropriately

Generating V2 Signatures

Use the Signature Helper for all V2 signatures:

import { useSignatureHelper } from '@/lib/signature-store'
import { FloatingSignatureHelper } from '@/components/signature-status-components'
 
function V2ConfigurationInterface() {
  const { openModal, isModalOpen, closeModal } = useSignatureHelper()
 
  return (
    <>
      <Button onClick={() => openModal()}>
        Generate V2 Signatures
      </Button>
 
      <FloatingSignatureHelper />
 
      {/* Use modal with pre-signed configurations */}
      <UpdateFundClaimConfigModal
        isOpen={isModalOpen}
        onClose={closeModal}
        projectAddress={projectAddress}
        // Pre-signed values will be auto-populated
      />
    </>
  )
}

Signature Status Display

Show V2 signature status with collapsible sections:

import { SignatureStatusDisplay } from '@/components/signature-status-display'
 
function ConfigurationPanel() {
  return (
    <div className="space-y-4">
      <h3>Configuration Signatures</h3>
      <SignatureStatusDisplay />
      
      {/* Displays:
        - Core Signatures (Config, NextConfig)
        - V2 Signatures (ConfigV2, FundClaimConfig, DistributorConfig)
        - Status badges for each signature
        - Collapsible sections for organization
      */}
    </div>
  )
}

Version-Aware Components

Conditional V2 Features

Render V2 features only for V2-initialized projects:

function ProjectActions({ 
  projectAddress,
  projectId 
}: {
  projectAddress: Address
  projectId: number
}) {
  const { isV2 } = useProjectVersion(projectAddress)
  const v2Config = useProjectV2Configuration(projectId)
 
  return (
    <div className="space-y-4">
      {/* Standard actions available for all versions */}
      <Button>Purchase Tokens</Button>
      <Button>Claim Tokens</Button>
 
      {/* V2-specific actions */}
      {isV2 && v2Config.status.hasConfig && (
        <>
          <Button>Claim Vested Tokens</Button>
          <Button>View Vesting Schedule</Button>
        </>
      )}
 
      {/* Project owner V2 actions */}
      {isV2 && isProjectOwner && (
        <>
          <Button>Configure Fund Vesting</Button>
          <Button>Claim Vested Funds</Button>
          <Button>Add Distributor</Button>
        </>
      )}
    </div>
  )
}

Version Badges

Display version information consistently:

function ProjectCard({ project }: { project: Project }) {
  const { version, isV2 } = useProjectVersion(project.address)
 
  return (
    <Card>
      <CardHeader>
        <div className="flex items-center justify-between">
          <CardTitle>{project.name}</CardTitle>
          <Badge variant={isV2 ? 'default' : 'secondary'}>
            {isV2 && <CheckCircle className="h-3 w-3 mr-1" />}
            V{version}
          </Badge>
        </div>
      </CardHeader>
      <CardContent>
        {isV2 && (
          <Alert className="border-blue-200 bg-blue-50">
            <Info className="h-4 w-4" />
            <AlertDescription>
              Enhanced V2 features: Vesting, Auto-transitions, Multi-distributors
            </AlertDescription>
          </Alert>
        )}
      </CardContent>
    </Card>
  )
}

Error Handling

V2-Specific Errors

Handle V2-specific error cases:

function V2ErrorBoundary({ children }: { children: React.ReactNode }) {
  const handleV2Error = (error: Error) => {
    if (error.message.includes('InvalidInitialization')) {
      return (
        <Alert className="border-red-200">
          <AlertTriangle className="h-4 w-4" />
          <AlertDescription>
            V2 features must be initialized before use.
            <Button onClick={initializeV2} className="mt-2">
              Initialize V2
            </Button>
          </AlertDescription>
        </Alert>
      )
    }
 
    if (error.message.includes('InvalidNonce')) {
      return (
        <Alert className="border-amber-200">
          <AlertTriangle className="h-4 w-4" />
          <AlertDescription>
            Invalid nonce. Please refresh and try again with the current nonce.
          </AlertDescription>
        </Alert>
      )
    }
 
    // Default error handling
    return <div>Error: {error.message}</div>
  }
 
  return (
    <ErrorBoundary fallback={handleV2Error}>
      {children}
    </ErrorBoundary>
  )
}

Best Practices

1. Always Check V2 Initialization

const v2Config = useProjectV2Configuration(projectId)
 
if (!v2Config.status.hasConfig) {
  return <InitializeV2Prompt />
}
 
// Proceed with V2 features

2. Use Current Nonces

// Always fetch current nonce from contract
const { nonce } = useNonce(projectAddress, signatureType, signerAddress)
 
// Don't hardcode or guess nonces

3. Handle Version Differences

// V2 requires only 2 signatures (manager + owner)
// V1 requires 3 signatures (manager + distributor + owner)
const requiredSignatures = isV2 ? 2 : 3

4. Validate Before Submission

// Check all preconditions
if (!v2Config.status.canUpdateFundClaimConfig) {
  toast.error(v2Config.status.fundClaimConfigUpdateError)
  return
}
 
// Proceed with update

5. Progressive Enhancement

// Provide V1 functionality as baseline
<StandardClaimButton />
 
// Enhance with V2 features when available
{isV2 && <VestedClaimButton />}

Complete Example

Full V2 Project Interface

'use client'
 
import { useState } from 'react'
import { useProjectVersion } from '@/lib/use-project-version'
import { useProjectV2Configuration } from '@/lib/progressive-ifif-v2-configuration-hooks'
import {
  InitializeV2Modal,
  UpdateFundClaimConfigModal,
  UpdateDistributorConfigModal,
  ClaimFundModal
} from '@/components/ifif-v2-project-client-modals'
 
export function ProjectV2Interface({ 
  projectAddress,
  projectId,
  projectOwner
}: {
  projectAddress: Address
  projectId: number
  projectOwner: Address
}) {
  const { version, isV2, isLoading: isLoadingVersion } = useProjectVersion(projectAddress)
  const v2Config = useProjectV2Configuration(projectId)
  
  const [activeModal, setActiveModal] = useState<string | null>(null)
 
  if (isLoadingVersion) {
    return <div>Loading project version...</div>
  }
 
  if (!isV2) {
    return (
      <Alert>
        <Info className="h-4 w-4" />
        <AlertDescription>
          This project uses V1. V2 features are not available.
        </AlertDescription>
      </Alert>
    )
  }
 
  return (
    <div className="space-y-6">
      {/* V2 Status */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <Badge variant="default">V{version}</Badge>
            IFIF V2 Enhanced Features
          </CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          {!v2Config.status.hasConfig ? (
            <Alert className="border-amber-200">
              <AlertTriangle className="h-4 w-4" />
              <AlertDescription>
                V2 features not initialized yet.
                <Button 
                  onClick={() => setActiveModal('initializeV2')}
                  className="mt-2"
                >
                  Initialize V2 Features
                </Button>
              </AlertDescription>
            </Alert>
          ) : (
            <Alert className="border-green-200">
              <CheckCircle className="h-4 w-4 text-green-600" />
              <AlertDescription>
                V2 features active with enhanced capabilities
              </AlertDescription>
            </Alert>
          )}
 
          {/* V2 Configuration Actions */}
          {v2Config.status.hasConfig && (
            <div className="grid grid-cols-2 gap-4">
              <Button 
                onClick={() => setActiveModal('fundVesting')}
                disabled={!v2Config.status.canUpdateFundClaimConfig}
              >
                Configure Fund Vesting
              </Button>
              
              <Button onClick={() => setActiveModal('addDistributor')}>
                Add Distributor
              </Button>
              
              <Button 
                onClick={() => setActiveModal('claimFunds')}
                disabled={!v2Config.fundClaimConfig}
              >
                Claim Vested Funds
              </Button>
            </div>
          )}
        </CardContent>
      </Card>
 
      {/* Modals */}
      <InitializeV2Modal
        isOpen={activeModal === 'initializeV2'}
        onClose={() => setActiveModal(null)}
        projectAddress={projectAddress}
        projectId={projectId}
        onSuccess={() => setActiveModal(null)}
      />
 
      <UpdateFundClaimConfigModal
        isOpen={activeModal === 'fundVesting'}
        onClose={() => setActiveModal(null)}
        projectAddress={projectAddress}
        projectId={projectId}
        projectOwner={projectOwner}
        onSuccess={() => setActiveModal(null)}
      />
 
      <UpdateDistributorConfigModal
        isOpen={activeModal === 'addDistributor'}
        onClose={() => setActiveModal(null)}
        mode="new"
        projectAddress={projectAddress}
        projectId={projectId}
        projectOwner={projectOwner}
        onSuccess={() => setActiveModal(null)}
      />
 
      <ClaimFundModal
        isOpen={activeModal === 'claimFunds'}
        onClose={() => setActiveModal(null)}
        projectAddress={projectAddress}
        projectId={projectId}
        projectOwner={projectOwner}
        onSuccess={() => setActiveModal(null)}
      />
    </div>
  )
}