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)}
/>
</>
)
}- Manager generates and signs the FundClaimConfig
- Manager signature is provided to the modal (via form field or signature helper)
- Project Owner approves by submitting the transaction
- 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- ClaimConfig: For investor token claims (factory operation, requires Manager role)
- FundClaimConfig: For project owner fund withdrawals (project operation, requires Project Owner)
- ClaimConfig uses
startEpoch+periodLengthonly - 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
}- Forms use
stringfor user input - Contracts use
bigintfor precision - Ponder returns
stringfor 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 features2. Use Current Nonces
// Always fetch current nonce from contract
const { nonce } = useNonce(projectAddress, signatureType, signerAddress)
// Don't hardcode or guess nonces3. Handle Version Differences
// V2 requires only 2 signatures (manager + owner)
// V1 requires 3 signatures (manager + distributor + owner)
const requiredSignatures = isV2 ? 2 : 34. Validate Before Submission
// Check all preconditions
if (!v2Config.status.canUpdateFundClaimConfig) {
toast.error(v2Config.status.fundClaimConfigUpdateError)
return
}
// Proceed with update5. 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>
)
}