IFIF Contract
The IFIF contract is the core crowdfunding project contract that manages the complete lifecycle of investment projects, from initialization through token distribution. It implements ERC721 NFTs to represent user allocations and provides a sophisticated multi-stage sale system with automatic DEX integration.
Overview
Contract Address: contracts/src/core/IFIF.sol
License: GPL-3.0-only
Solidity Version: 0.8.19
Key Features
- Multi-Stage Sale System: Private sale → Public sale → Success/Failure determination
- NFT Allocation Tracking: ERC721 tokens representing purchase allocations with split/merge functionality
- Whitelist Integration: Merkle tree-based access control for private and public sales
- DEX Integration: Automatic pair creation and liquidity provision upon successful funding
- EIP712 Signatures: Secure configuration updates through multi-signature validation with nonce-based replay attack prevention
- Signature Nonce System: Independent nonce tracking per signature type prevents signature reuse and enables batch invalidation
- UUPS Upgradeable: Admin-controlled contract upgrades (only during INIT stage)
Project Lifecycle
The IFIF contract manages projects through distinct stages:
Stage Flow
NONE → INIT → PRIVATE_SALE → PUBLIC_SALE → SALE_SUCCESSED/FAILED → CLAIMStage Descriptions
NONE (0)
- Purpose: Initial uninitialized state
- Permissions: No operations allowed
- Transition: Manual initialization by factory
INIT (1)
- Purpose: Project configuration and preparation
- Permissions: Configuration updates, manual stage transitions by owner
- Duration: Until owner manually starts private sale
- Transition: Manual call to
startPrivateSale()by project owner
PRIVATE_SALE (2)
- Purpose: Whitelist-based early investment phase with bonus
- Permissions: Whitelisted addresses can purchase with bonus percentage
- Duration:
privateSaleTimeseconds from start - Requirements: Valid Merkle proof for private sale section
- Transition: Manual call to
startPublicSale()by project owner
PUBLIC_SALE (3)
- Purpose: Open investment phase for all whitelisted participants
- Permissions: Whitelisted addresses can purchase (no bonus)
- Duration:
publicSaleTimeseconds from start - Requirements: Valid Merkle proof for public sale section
- Transition: Manual call to
endSales()by owner, or manual call topublicEndSales()by anyone after 60 days past scheduled end time
SALE_SUCCESSED (4)
- Purpose: Successful funding completion
- Trigger: Exact funding target reached (
totalPurchase == fundAmount) - Actions: Funds transferred to project owner, NFT claiming enabled
- Permissions: Users can claim NFTs, distributor can deposit tokens
SALE_FAILED (5)
- Purpose: Failed funding (target not reached)
- Trigger: Sales ended without reaching exact funding target
- Actions: Refund mechanism enabled
- Permissions: Purchase refunds available for all participants
CLAIM (6)
- Purpose: Token distribution phase
- Requirements: Successful sale completion and distributor token deposit
- Actions: NFT conversion to project tokens based on weight allocation
- Duration: Permanent claiming window
Core Data Structures
Config
struct Config {
address fundToken; // Token used for funding (USDC, USDT, etc.)
uint256 projectId; // Unique identifier for the project
uint256 fundAmount; // Target funding amount to raise
uint256 privateSaleTime; // Duration of private sale in seconds
uint256 privateBonusPercent; // Bonus percentage for private sale (0-99)
uint256 publicSaleTime; // Duration of public sale in seconds
uint256 desiredEndEpoch; // Maximum project end timestamp
}NextConfig
struct NextConfig {
address projectOwner; // Project owner receiving funds and controlling sales
address distributor; // Distributor depositing tokens and managing distribution
uint256 distributorFeePercent; // Percentage fee taken by distributor (0-99)
uint256 projectOwnerFeePercent; // Percentage fee for project owner (0-99)
uint256 platformFeePercent; // Percentage fee for platform (0-99)
uint256 pairPrice; // Token price for DEX pair (scaled by 1e18)
uint256 nonce; // Nonce for replay attack prevention (must match or exceed current nonce)
}Nonce Field: The nonce field in NextConfig enables signature replay attack prevention. For initialization, use nonce: 0. For updates, read the current nonce from the contract via nonces(0, projectOwner) where 0 is the NONCE_NEXT_CONFIG constant. The contract validates that the provided nonce is greater than or equal to the current nonce, then increments it to providedNonce + 1, invalidating all signatures with lower nonces.
Purchase Tracking
mapping(address user => uint256 weight) public purchaseWeights; // Weighted allocation including bonuses
mapping(address user => uint256 amount) public purchases; // Raw purchase amounts for refunds
mapping(uint256 tokenId => uint256 weight) public nftWeights; // NFT token weight mappingView Functions
Project State
stage() → Stage
Returns the current stage of the project lifecycle.
totalPurchase() → uint256
Returns the total amount purchased across all participants in funding tokens.
totalWeight() → uint256
Returns the total weight from all participant investments (including bonuses).
activeSaleEndTime() → uint256
Returns the timestamp when the current active sale ends.
token() → address
Returns the address of the deployed project token (zero if not deployed).
earningsMultiplier() → uint256
Returns the earnings multiplier for token claim calculations.
dexPair() → address
Returns the address of the created DEX liquidity pair.
owner() → address
Returns the current project owner address (from NextConfig, not contract owner).
User Data
purchaseWeights(address user) → uint256
Returns the purchase weight for a specific user (including bonuses).
purchases(address user) → uint256
Returns the purchase amount for a specific user in funding tokens.
nftWeights(uint256 tokenId) → uint256
Returns the weight associated with a specific NFT.
Nonce Management
nonces(uint8 signatureType, address signer) → uint256
Returns the current nonce value for a specific signature type and signer address.
Parameters:
signatureType: Type identifier (0 = NONCE_NEXT_CONFIG for NextConfig signatures)signer: Address whose nonce to query (typically the project owner)
Usage: Read this value before creating update signatures to ensure proper nonce sequencing and replay attack prevention.
Nonce Constants:
NONCE_NEXT_CONFIG = 0: Used for NextConfig struct signatures
Nonce Behavior:
- Initialization: Start with nonce 0 for first deployment
- Updates: Read current nonce, include it in signature, contract increments to
providedNonce + 1 - Skipping: Using a higher nonce (e.g., 3) when current is 0 will set nonce to 4, invalidating signatures with nonces 0, 1, and 2
EIP-712 Type Hashes
CONFIG_TYPEHASH() → bytes32
Returns the EIP-712 typehash for Config struct (does not include nonce).
NEXT_CONFIG_TYPEHASH() → bytes32
Returns the EIP-712 typehash for NextConfig struct (includes nonce field for replay attack prevention).
ERC721 Metadata
name() → string
Returns the NFT collection name (format: "projectName NFT").
symbol() → string
Returns the NFT collection symbol (format: "projectSymbolN").
tokenURI(uint256 tokenId) → string
Returns the metadata URI for a specific NFT (format: "baseURI/projectId/tokenId").
Core Functions
Initialization
initialize()
function initialize(
Config calldata config,
NextConfig calldata nextConfig,
bytes calldata configSignature,
bytes calldata nextConfigSignature
) externalPurpose: Initializes a new IFIF project with configuration and EIP-712 signature validation.
Parameters:
config: Core project configuration (funding targets, sale durations, etc.)nextConfig: Mutable configuration (stakeholders, fees, pricing, nonce)configSignature: EIP-712 signature from manager validating confignextConfigSignature: EIP-712 signature from manager validating nextConfig (includes nonce validation)
Access: Factory contract only during deployment
Validations:
- Valid manager signatures for both config structs
- Nonce validation (must use nonce: 0 for initialization)
- Distributor has DISTRIBUTOR_ROLE
- Total fee percentages < 100%
- Valid bonus percentage < 100%
- Desired end epoch in the future
Nonce Initialization: The nextConfig must include nonce: 0 for first-time project deployment. The contract validates and consumes this nonce, setting the next expected nonce to 1.
startPrivateSale()
function startPrivateSale() external isStage(Stage.INIT) onlyOwnerPurpose: Manually starts the private sale phase.
Requirements:
- Project must be in INIT stage
- Only project owner can call
Actions:
- Transitions to PRIVATE_SALE stage
- Sets
activeSaleEndTimeto current time +privateSaleTime - Emits StageUpdated event
startPublicSale()
function startPublicSale() external isStage(Stage.PRIVATE_SALE) onlyOwnerPurpose: Manually starts the public sale phase.
Requirements:
- Project must be in PRIVATE_SALE stage
- Only project owner can call
- Funding target not yet reached
Actions:
- Transitions to PUBLIC_SALE stage
- Sets
activeSaleEndTimeto current time +publicSaleTime - Emits StageUpdated event
Purchase Functions
purchase()
function purchase(
uint256 amount,
bytes32[] calldata proof
) external isSaleStage nonReentrantPurpose: Allows whitelisted users to purchase project allocations with funding tokens.
Parameters:
amount: Purchase amount in funding tokens (not ETH)proof: Merkle proof for whitelist verification
Requirements:
- Must be in PRIVATE_SALE or PUBLIC_SALE stage
- Valid whitelist proof for current sale section (section 1 for private sale, section 2 for public sale)
- Purchase amount > 0
- Sale time not expired
- Project deadline not exceeded
- Total purchase + amount ≤ funding target
Actions:
- Validates whitelist membership
- Calculates weight with bonus for private sale
- Updates purchase tracking mappings
- Transfers funding tokens from user
- Updates total purchase amount
Events: Emits Purchased event
NFT Management
claimNFT()
function claimNFT() external isStage(Stage.SALE_SUCCESSED)Purpose: Claims NFT representing user's allocation after successful funding.
Requirements:
- Caller must have valid purchase record (purchases[msg.sender] > 0)
- Project must be in SALE_SUCCESSED stage
- User hasn't already claimed NFT (can only be called once per user)
Actions:
- Mints NFT with weight equal to user's purchase weight
- Stores NFT weight in nftWeights mapping
- Clears user's purchase and purchaseWeights data
- Increments _currentTokenId counter
Events: Emits NFTClaimed event with user address and token ID
Note: This function can only be called once per user as it deletes their purchase data
splitNFT()
function splitNFT(
uint256 tokenId,
uint256[] calldata tokenWeights
) external isStage(Stage.SALE_SUCCESSED)Purpose: Splits an existing NFT allocation into multiple smaller NFTs.
Parameters:
tokenId: NFT to splittokenWeights: Array of weights for new NFTs (maximum 3 NFTs)
Requirements:
- Caller must own the NFT
- Project must be in SALE_SUCCESSED stage
- Maximum 3 new NFTs can be created
- Sum of weights must be ≤ original NFT weight (allows partial splits)
- All weights must be greater than zero
Actions:
- Burns original NFT if total weight equals original weight
- Reduces original NFT weight if partial split (total weight < original)
- Mints new NFTs with specified weights
- Maintains total allocation weight
Events: Emits NFTSplitted event
Important: NFT split operations are only available during SALE_SUCCESSED stage. Once the project transitions to CLAIM stage, NFTs can only be converted to tokens via convertNFT().
mergeNFT()
function mergeNFT(uint256[] calldata tokenIds) external isStage(Stage.SALE_SUCCESSED)Purpose: Merges multiple NFTs owned by the caller into a single NFT.
Parameters:
tokenIds: Array of NFT IDs to merge (maximum 3 NFTs)
Requirements:
- Caller must own all specified NFTs
- Project must be in SALE_SUCCESSED stage
- Maximum 3 NFTs can be merged
- At least 1 NFT must be provided
Actions:
- Burns all specified NFTs
- Mints single NFT with combined weight
- Maintains total allocation weight
Events: Emits NFTMerged event
Important: NFT merge operations are only available during SALE_SUCCESSED stage. Once the project transitions to CLAIM stage, NFTs can only be converted to tokens via convertNFT().
Token Distribution
deposit()
function deposit(uint256 amount) external isStage(Stage.SALE_SUCCESSED) nonReentrantPurpose: Distributor deposits tokens to enable claiming and create DEX liquidity.
Parameters:
amount: Total amount of tokens to deposit for distribution and liquidity
Requirements:
- Project must be in SALE_SUCCESSED stage
- Only distributor can call this function
- Amount must be greater than zero
Actions:
- Calculates fee distributions (project owner, platform, liquidity)
- Transfers fees to respective parties
- Deploys project token via factory
- Creates DEX pair and adds liquidity
- Transitions to CLAIM stage
- Sets
earningsMultiplierfor proportional token claiming
claim()
function claim() external isStage(Stage.CLAIM)Purpose: Allows users to claim tokens directly from their purchase weights.
Requirements:
- Project must be in CLAIM stage
- Caller must have purchase weights > 0
- User hasn't already claimed tokens
Actions:
- Calculates token amount based on user's purchase weight and earnings multiplier
- Clears user's purchase and purchaseWeights data
- Transfers calculated token amount to user
Events: Emits Claimed event
Note: Alternative to NFT flow - users can claim directly without first claiming NFT
convertNFT()
function convertNFT(uint256 tokenId) external isStage(Stage.CLAIM)Purpose: Converts NFT to project tokens based on weight allocation.
Parameters:
tokenId: NFT ID representing allocation
Requirements:
- Caller must own the NFT
- Project must be in CLAIM stage
- Distributor must have deposited tokens
Calculations:
- Token amount = calculated using NFT weight, earnings multiplier, and pair price
Actions:
- Calculates token amount based on NFT weight
- Burns converted NFT
- Transfers calculated token amount to user
Events: Emits NFTConverted event
Note: Alternative to direct claim - users first claim NFT, then convert it to tokens
Refund System
refund()
function refund() external isStage(Stage.SALE_FAILED)Purpose: Refunds purchase amount for failed projects.
Requirements:
- Caller must have valid purchase record
- Project must be in SALE_FAILED stage
- User hasn't already claimed refund
Actions:
- Transfers purchase amount back to caller in funding tokens
- Clears user's purchase records
- Updates refund tracking
Events: Emits Refunded event
Administrative Functions
endSales()
function endSales() external onlyOwnerPurpose: Manually ends the current sale phase and determines project outcome.
Requirements:
- Must be in PRIVATE_SALE or PUBLIC_SALE stage
- Only project owner can call
Logic:
- If
totalPurchase == fundAmount: transitions to SALE_SUCCESSED, transfers funds to owner - If
totalPurchase != fundAmount: transitions to SALE_FAILED
publicEndSales()
function publicEndSales() externalPurpose: Emergency function to end stuck sales after 60 days past scheduled end.
Requirements:
- Must be 60+ days past
activeSaleEndTime - Anyone can call this function
Logic: Same as endSales() but with emergency timing validation
updateConfig()
function updateConfig(
NextConfig calldata nextConfig,
bytes[] calldata signatures
) externalPurpose: Updates mutable project configuration with multi-signature validation and nonce-based replay attack prevention.
Parameters:
nextConfig: New configuration values including current or higher noncesignatures: Array of exactly 3 signatures [manager, current distributor, current project owner]
Requirements:
- Must have exactly 3 signatures
- Cannot be called during CLAIM stage
- Nonce must be greater than or equal to current nonce value
- Valid signatures from:
- Manager (validates with manager role)
- Current distributor (validates against current distributor address)
- Current project owner (validates against current owner address)
- New distributor must have DISTRIBUTOR_ROLE
- Total fee percentages < 100%
Nonce Workflow:
- Read current nonce:
uint256 currentNonce = ifif.nonces(0, projectOwner) - Include nonce in nextConfig:
nextConfig.nonce = currentNonce(or higher to skip nonces) - Create signatures with nonce-aware digest
- Contract validates nonce ≥ current, then sets nonce to
providedNonce + 1 - All signatures with nonces ≤ providedNonce become invalid
Security: Multi-signature validation with nonce-based replay attack prevention ensures only fresh, authorized updates are processed
Events: Emits NonceConsumed(0, projectOwner, providedNonce) to track nonce usage
DEX Integration
Automatic Liquidity Provision
When distributor calls deposit() after successful funding:
- Fee Distribution: Calculates and distributes fees to project owner and platform
- Token Deployment: Factory deploys project token with calculated mint amount
- Pair Creation: Creates DEX pair for fundToken/projectToken
- Liquidity Addition: Adds calculated liquidity amounts to the pair
- Earnings Setup: Calculates
earningsMultiplierfor proportional token distribution
Liquidity Calculations
// Fee calculations from deposit amount
uint256 projectOwnerShare = shareAmount(amount, distributorFee, projectOwnerFee);
uint256 platformShare = shareAmount(amount, distributorFee, platformFee);
uint256 liquidityAmount = amount - projectOwnerShare - platformShare;
// Token amounts for DEX pair
uint256 pairTokenAmount = pairLiquidityAmount * pairPrice / 1e18;
uint256 totalTokenMint = pairTokenAmount * 2 + (pairTokenAmount * 2 / 1000); // Double for liquidity + users, plus 0.1% bufferSupported DEX Protocols
- Uniswap V2: Primary DEX integration
- Compatible Forks: Any Uniswap V2-compatible DEX
- Pair Structure: FundToken/ProjectToken pairs (not ETH pairs)
Security Features
Access Control
- Role-based permissions: Factory, owner, and admin-specific functions
- Stage-based restrictions: Operations only available in appropriate stages
- Ownership validation: NFT ownership required for user actions
Signature Verification & Replay Attack Prevention
- EIP712 compliance: Standardized signature format for all configuration updates
- Manager signature requirement: Manager signatures required for config validation
- Nonce-based replay protection: Independent nonce tracking per signature type prevents signature reuse
- SignatureNonceManager inheritance: Provides unified nonce management system across all signature types
- Nonce skipping capability: Allows intentional invalidation of multiple pending signatures by using higher nonces
- Sequential validation: Enforces nonce ≥ current nonce, then increments to providedNonce + 1
- Event transparency: NonceConsumed events provide audit trail of nonce usage
Reentrancy Protection
- Critical functions protected: All token transfer operations use reentrancy guards
- State updates first: Balance updates before external calls
- Safe transfer patterns: Uses SafeTransferLib for secure transfers
Input Validation
- Comprehensive checks: All function parameters validated
- Custom errors: Gas-efficient error reporting
- Edge case handling: Prevents division by zero and overflow conditions
Events
Core Events
event Purchased(address indexed user, uint256 amount);
event Refunded(address indexed user, uint256 amount);
event NFTClaimed(address indexed user, uint256 indexed tokenId);
event NFTSplitted(uint256 indexed tokenId, uint256[] newTokenIds);
event NFTMerged(uint256[] tokenIds, uint256 indexed newTokenId);
event Deposited(uint256 amount);
event Claimed(address indexed user, uint256 amount);
event NFTConverted(address indexed user, uint256 indexed tokenId, uint256 amount);
event ConfigUpdated(address newProjectOwner, address newDistributor);
event StageUpdated(Stage newStage);
event DexPairCreated(address indexed pair, address indexed token, address indexed fundToken);
event NonceConsumed(uint8 indexed signatureType, address indexed signer, uint256 nonce);Nonce Management Event
The NonceConsumed event is emitted whenever a nonce is successfully validated and incremented:
signatureType: The type of signature (0 for NONCE_NEXT_CONFIG)signer: The address whose nonce was consumed (typically project owner)nonce: The nonce value that was consumed (before increment)
This event provides transparency for nonce usage and helps with off-chain tracking and debugging.
Integration Examples
Frontend Integration
// Project lifecycle management
const project = await ethers.getContractAt("IFIF", projectAddress);
// Check current stage and nonce state
const currentStage = await project.stage();
const projectOwner = await project.owner();
const currentNonce = await project.nonces(0, projectOwner); // 0 = NONCE_NEXT_CONFIG
console.log(`Current nonce: ${currentNonce}`);
// Start private sale (project owner only)
if (currentStage === 1) { // INIT
const startPrivateTx = await project.startPrivateSale();
}
// Start public sale (project owner only)
if (currentStage === 2) { // PRIVATE_SALE
const startPublicTx = await project.startPublicSale();
}
// Purchase during sale with funding token (not ETH)
const fundToken = await ethers.getContractAt("ERC20", fundTokenAddress);
await fundToken.approve(projectAddress, purchaseAmount);
const purchase = await project.purchase(purchaseAmount, merkleProof);
// Claim NFT after successful funding
if (currentStage === 4) { // SALE_SUCCESSED
const claimNFT = await project.claimNFT();
}
// Distributor deposits tokens to enable claiming
if (currentStage === 4) { // SALE_SUCCESSED
const token = await ethers.getContractAt("ERC20", tokenAddress);
await token.approve(projectAddress, depositAmount);
const deposit = await project.deposit(depositAmount);
}
// Convert NFT to tokens during CLAIM phase (NFT flow)
if (currentStage === 6) { // CLAIM
// Option 1: Claim NFT first, then convert to tokens
const claimNFT = await project.claimNFT(); // Must be in SALE_SUCCESSED stage
// ... later when stage becomes CLAIM ...
const convert = await project.convertNFT(tokenId);
// Option 2: Direct token claiming without NFT (during CLAIM stage)
const directClaim = await project.claim(); // Direct token claim from purchase weights
}
// Split NFT allocation
const split = await project.splitNFT(
tokenId,
[weight1, weight2] // Weight values, not amounts
);
// Update project configuration with nonce-based replay attack prevention
const currentNonce = await project.nonces(0, projectOwner); // Read current nonce
const newNextConfig = {
projectOwner: projectOwner,
distributor: newDistributor,
distributorFeePercent: 3,
projectOwnerFeePercent: 5,
platformFeePercent: 2,
pairPrice: ethers.parseEther("0.1"),
nonce: currentNonce // Use current nonce or higher to skip nonces
};
// Create EIP-712 signatures with nonce-aware digest
const signatures = await createUpdateConfigSignatures(project.address, newNextConfig);
// Update configuration (nonce will be incremented to currentNonce + 1)
const updateTx = await project.updateConfig(newNextConfig, signatures);
// After update, nonce is now currentNonce + 1
const newNonce = await project.nonces(0, projectOwner);
console.log(`Nonce after update: ${newNonce}`); // currentNonce + 1
// Advanced: Skip multiple nonces to invalidate pending signatures
const skipToNonce = currentNonce + 5; // Skip to nonce 5
newNextConfig.nonce = skipToNonce;
// Create signatures and update...
// This will set nonce to 6, invalidating all signatures with nonces 0-5Query Functions
// Check project stage and metrics
Stage currentStage = ifif.stage();
uint256 totalPurchased = ifif.totalPurchase();
uint256 totalWeights = ifif.totalWeight();
uint256 activeSaleEnd = ifif.activeSaleEndTime();
// Get project owner (from NextConfig, not traditional contract owner)
address projectOwner = ifif.owner(); // Returns _nextConfig.projectOwner
// Get user purchase information
uint256 userWeight = ifif.purchaseWeights(userAddress);
uint256 userPurchase = ifif.purchases(userAddress);
// Get NFT information
uint256 nftWeight = ifif.nftWeights(tokenId);
// ERC721 metadata functions
string memory collectionName = ifif.name(); // Returns "projectName NFT"
string memory collectionSymbol = ifif.symbol(); // Returns "projectSymbolN"
string memory nftMetadataURI = ifif.tokenURI(tokenId); // Returns "baseURI/projectId/tokenId"
// Check if claiming is active
address projectToken = ifif.token();
uint256 earningsMultiplier = ifif.earningsMultiplier();
address dexPair = ifif.dexPair();Testing Coverage
The IFIF contract maintains 100% test coverage including:
- Lifecycle transitions: All stage changes and validations
- Purchase scenarios: Valid and invalid purchase attempts
- NFT operations: Split, merge, and transfer functionality
- Token claiming: Proportional distribution calculations
- Refund mechanisms: Failed project recovery
- Security tests: Access control and reentrancy protection
- Edge cases: Boundary conditions and error scenarios
Deployment Considerations
Gas Optimization
- Clone pattern: Deployed via factory using LibClone for efficiency
- Packed structs: Optimized storage layout for gas savings
- Custom errors: Gas-efficient error reporting
Network Compatibility
- EVM compatible: Works on any Ethereum-compatible network
- DEX integration: Requires compatible Uniswap V2-style DEX
- Block time considerations: Timestamp validations account for network block times
For detailed implementation examples and integration patterns, see the Examples section.