FeeRouter Smart Contract
Technical documentation for the FeeRouter smart contract - routes commission payments to referrers based on ReplyCorp attribution data. Covers architecture, security, deployment, and integration.
Table of Contents
Overview
The FeeRouter contract is a best-effort fee distribution system designed to route commission payments to influencers based on attribution data provided by ReplyCorp. The contract implements a three-phase distribution model:
Attribution Update Phase: ReplyCorp (as attribution updater) provides attribution percentages and commission amounts
Distribution Start Phase: Authorized signer pulls tokens and initiates distribution
Batch Processing Phase: Fees are distributed in batches (up to 200 recipients per batch) with best-effort transfers
Core Principles
Best-Effort Distribution: Transfers may fail silently and accumulate as dust. The contract does NOT guarantee delivery to all recipients.
Dust Ownership: All dust (rounding remainders, failed transfers, skipped batches) belongs permanently to the contract owner.
Bounded Lifetime: Designed for per-campaign/per-client/per-epoch use. Deploy new instances to avoid unbounded storage growth.
ERC20 Only: No native token support - uses standard ERC20 tokens (primarily USDC/USDT).
Architecture
Complete Flow Diagram

Role-Based Access Control
The contract implements three distinct roles:
Owner (Client)
Deploys the contract
Sets authorized addresses (attribution updater, distribution signer, ReplyCorp wallet)
Withdraws accumulated dust
Rescues accidentally sent tokens
Transfers ownership
Attribution Updater (ReplyCorp)
Updates attribution data for conversions
Provides attribution weights (based on totalVolume) and commission amounts
Cannot modify data after distribution starts
Distribution Signer (Authorized Wallet)
Starts distribution by pulling tokens
Processes batches of recipients
Finalizes distributions
Must provide hash verification to ensure they reviewed attribution data
State Machine

Data Flow
Key Design Decisions
1. Attribution Weights vs. Payout Amounts
Decision: Attribution weights are calculated from totalVolume (who influenced the sale), but payout amounts are calculated from commission (what the customer wants to distribute).
Rationale:
Attribution reflects influence on the sale (totalVolume)
Payout reflects customer's commission budget (commission)
This separation allows flexible commission structures while maintaining accurate attribution
Implementation:
2. Best-Effort Distribution Model
Decision: Failed transfers are skipped and accumulated as dust rather than reverting the entire distribution.
Rationale:
Some recipients may have invalid addresses or reject transfers
Partial distributions are better than no distribution
Failed amounts are tracked and can be handled off-chain
Implementation:
Uses low-level
call()to catch transfer failuresFailed transfers accumulate as
failedSoFarandaccumulatedDustSuccessful transfers continue even if some fail
3. Batch Processing
Decision: Process recipients in batches of up to 200 to avoid gas limit issues.
Rationale:
Large distributions (1000+ recipients) would exceed gas limits
Batches can be processed in any order
Batches can be skipped (intentional design for best-effort model)
Implementation:
Each batch processes up to 200 recipients
Batch completion tracked per conversion and version
Batches can be processed out of order or skipped entirely
4. Reserved Balance System
Decision: Track reserved balance per conversion and globally to prevent premature rescue.
Rationale:
Prevents owner from rescuing funds mid-distribution
Enables efficient
rescueTokens()calculationMaintains accounting invariants
Implementation:
reservedBalanceper conversion tracks undistributed commissiontotalReservedBalancetracks across all active conversionsReserved balance decreases as batches complete
Remaining reserved balance converts to dust on finalization
5. Attribution Hash Verification
Decision: Distribution signer must provide matching hash when starting distribution.
Rationale:
Ensures signer reviewed exact attribution data before distributing
Prevents distribution with stale or incorrect data
Provides cryptographic proof of review
Implementation:
Hash computed from:
keccak256(conversionId, wallets[], weights[], commission, replyCorpFee)Hash stored in
attributionHashfieldstartDistribution()requires matching hash
6. Route Versioning
Decision: Increment route version on each attribution update to invalidate old batch completion flags.
Rationale:
Prevents batch completion flags from persisting across attribution updates
Ensures batch tracking is always current
Prevents replay attacks with old batch indices
Implementation:
routeVersion[conversionId]increments on eachupdateAttribution()Batch completion tracked per version:
batchCompleted[conversionId][version][batchIndex]
Contract Specification
State Variables
Structs
Distribution
AttributionData
Functions
Owner Functions
setAttributionUpdater(address newUpdater)
Access: Owner only
Purpose: Set/update ReplyCorp attribution updater address
Validation: New updater cannot be zero address
Event:
AttributionUpdaterUpdated
setDistributionSigner(address newSigner)
Access: Owner only
Purpose: Set/update authorized distribution signer
Validation:
Cannot be zero address
Cannot be token contract address
Cannot be contract address
Event:
DistributionSignerUpdated
setReplyCorpWallet(address newWallet)
Access: Owner only
Purpose: Update ReplyCorp wallet address
Validation:
Cannot be zero address
Cannot be token contract address
Cannot be contract address
Event:
ReplyCorpWalletUpdated
transferOwnership(address newOwner)
Access: Owner only
Purpose: Transfer contract ownership
Validation: New owner cannot be zero address
Event:
OwnershipTransferred
withdrawDust(uint256 amount)
Access: Owner only
Purpose: Withdraw accumulated dust
Validation:
Amount must be > 0
Amount must be <= accumulatedDust
Reentrancy: Protected
Event:
DustWithdrawn
rescueTokens(uint256 amount)
Access: Owner only
Purpose: Rescue accidentally sent tokens
Validation:
Amount must be > 0
Amount must be <= rescuable balance
Rescuable = contract balance - accumulatedDust - totalReservedBalance
Reentrancy: Protected
Event:
TokensRescued
Attribution Updater Functions
updateAttribution(bytes32 conversionId, Distribution[] calldata distributions, uint256 commission, uint256 replyCorpFee)
Access: Attribution updater only
Purpose: Update attribution data for a conversion
Validation:
ConversionId cannot be zero
Distribution signer must be set
Distributions array cannot be empty
Commission must be > 0
Commission must not cause overflow
ReplyCorp fee must not cause overflow
Wallets must be sorted in ascending order (deterministic)
Wallets cannot be zero, token address, or contract address
Attribution weights must sum to 10000 (100%)
Attribution must not already be distributed
Distribution must not have started
Reserved balance must be zero (no stale state)
Side Effects:
Increments route version
Clears existing distributions
Computes attribution hash
Stores new attribution data
Resets distribution state
Event:
AttributionUpdated(includes computed amounts and hash)
Distribution Signer Functions
startDistribution(bytes32 conversionId, uint256 totalAmount, bytes32 expectedHash)
Access: Distribution signer only
Purpose: Start distribution by pulling tokens
Validation:
ConversionId cannot be zero
Attribution data must exist
Distribution must not already be started
Distribution must not already be finalized
Total amount must match stored totalAmount
Expected hash must match stored attributionHash
Side Effects:
Pulls tokens from signer via
transferFrom(signer must approve first)Transfers ReplyCorp fee immediately
Reserves commission balance
Marks distribution as started
Reentrancy: Protected
Event:
DistributionStarted
processBatch(bytes32 conversionId, uint256 batchIndex)
Access: Distribution signer only
Purpose: Process a batch of recipients (up to 200)
Validation:
ConversionId cannot be zero
Attribution data must exist
Distribution must be started
Distribution must not be finalized
Batch must not already be completed
Batch index must be in range
Side Effects:
Processes up to 200 recipients
Uses best-effort transfers (skips failures)
Updates paidSoFar and failedSoFar
Releases reserved balance for successful transfers
Accumulates failed transfers as dust
Marks batch as completed
Reentrancy: Protected
Event:
BatchDistributed(includes failure counts)
finalizeDistribution(bytes32 conversionId)
Access: Distribution signer only
Purpose: Finalize distribution and convert remaining reserved balance to dust
Validation:
ConversionId cannot be zero
Distribution must be started
Distribution must not already be finalized
Side Effects:
Converts remaining reserved balance to dust
Releases reserved balance
Marks distribution as finalized
Validates invariants
Reentrancy: Protected
Event:
FeesDistributed(includes total dust)
View Functions
getDistributionRoute(bytes32 conversionId)
Returns: Complete distribution route including distributions array, amounts, hash, and status
Purpose: Read attribution data and distribution status
isBatchCompleted(bytes32 conversionId, uint256 batchIndex)
Returns: Whether the specified batch has been completed
Purpose: Check batch completion status (uses current route version)
getBatchCount(bytes32 conversionId)
Returns: Number of batches required (ceiling of distributions.length / 200)
Purpose: Calculate total batches needed
Security Model
Access Control
Owner Functions: Only owner can modify authorized addresses and withdraw dust
Attribution Updater: Only ReplyCorp can update attribution data
Distribution Signer: Only authorized signer can start/process/finalize distributions
Input Validation
Zero address checks on all address parameters
Overflow protection on arithmetic operations
Array length validation
State validation (cannot update after distribution starts)
Hash verification (signer must provide matching hash)
Reentrancy Protection
All external functions use
nonReentrantmodifierUses OpenZeppelin's
ReentrancyGuardSafeERC20 for token transfers
State Invariants
Reserved Balance Invariant:
data.reservedBalance == data.commissionafterstartDistribution()Total Reserved Invariant:
totalReservedBalance >= data.reservedBalance(global >= per-conversion)Contract Balance Invariant:
contractBalance >= accumulatedDust(after finalization)Theoretical Payout Invariant:
theoreticalTotal <= commission(floor division ensures this)
Hash Verification
Attribution hash computed deterministically from conversion data
Signer must provide matching hash when starting distribution
Prevents distribution with stale or incorrect data
Batch Completion Tracking
Batches tracked per conversion and version
Version increments on attribution update (invalidates old batch flags)
Prevents replay attacks with old batch indices
Dust Handling Policy
What is Dust?
Dust consists of:
Rounding Remainders: Floor division of commission creates small remainders
Failed Transfers: Transfers that fail (invalid addresses, rejections)
Skipped Batches: Batches that are never processed
Unreachable Recipients: Recipients with zero payout (due to floor division)
Dust Accumulation
Dust accumulates in
accumulatedDustcounterFailed transfers added immediately during batch processing
Remaining reserved balance converted to dust on finalization
Dust is tracked explicitly (never inferred from balance)
Dust Ownership
All dust belongs permanently to the contract owner
ReplyCorp and influencers have no claim on undelivered funds
Owner can withdraw dust via
withdrawDust()Dust withdrawal reduces contract balance but not reserved balance
Example Dust Scenarios
Scenario 1: Rounding Dust
Scenario 2: Failed Transfer
Scenario 3: Skipped Batch
Batch Processing Model
Batch Size
200 recipients per batch (gas limit consideration)
Batches numbered starting from 0
Last batch may have fewer than 200 recipients
Batch Processing Rules
Order Independence: Batches can be processed in any order
Skippable: Batches can be skipped entirely
Idempotent: Processing the same batch twice is prevented
Version-Aware: Batch completion tracked per route version
Batch Completion Detection
Contract tracks completion per batch index
isBatchCompleted()checks current route versiongetBatchCount()calculates total batches needed
Finalization
Must be called explicitly (does not auto-finalize)
Can be called even if zero batches processed (converts entire commission to dust)
Converts remaining reserved balance to dust
Marks distribution as finalized
Best-Effort Behavior
Failed transfers are skipped (not reverted)
Successful transfers continue even if some fail
Failed amounts accumulate as dust
Partial distributions are allowed
Deployment & Initialization
Deployment Steps
Deploy Contract
Client deploys with their wallet (becomes owner)
Token address is immutable
ReplyCorp wallet can be updated later
Set Attribution Updater
Owner sets ReplyCorp address as attribution updater
Set Distribution Signer
Owner sets authorized wallet that will call distribution functions
Set ReplyCorp Wallet (if different from constructor)
Owner can update ReplyCorp wallet address
Initialization Checklist
Integration Guide
1. Attribution Update (ReplyCorp)
2. Distribution Start (Authorized Signer)
3. Batch Processing (Authorized Signer)
4. Finalization (Authorized Signer)
5. Dust Withdrawal (Owner)
Version History
VERSION 1 (Current): Initial implementation with batch processing and best-effort distribution
Testing
The contract includes comprehensive test coverage:
77 passing tests
91.45% branch coverage (FeeRouter.sol)
89.38% overall branch coverage
100% statement, function, and line coverage
Test file: packages/contracts/test/FeeRouter.test.ts
Gas Analysis
Gas costs were measured using Hardhat's gas reporting. All measurements are with optimizer enabled (200 runs).
Attribution Update Gas Costs
1
~245,000
Base cost for attribution update
10
~680,000
Linear scaling with recipient count
50
~2,614,000
Storage costs increase with array size
200
~9,872,000
Maximum single-batch size
Key Observations:
Gas scales approximately linearly with recipient count
Each additional recipient adds ~49,000-50,000 gas
Storage costs dominate for large recipient lists
Distribution Gas Costs
startDistribution()
~165,000
Pulls tokens, transfers ReplyCorp fee
processBatch() (1 recipient)
~120,000
Base batch processing cost
processBatch() (10 recipients)
~401,000
~28,000 gas per recipient
processBatch() (50 recipients)
~1,652,000
~30,000 gas per recipient
processBatch() (200 recipients)
~6,345,000
~31,000 gas per recipient
finalizeDistribution()
~49,000
Converts remaining to dust
Key Observations:
startDistribution()has fixed cost regardless of recipient countprocessBatch()gas scales linearly: ~30,000 gas per recipientfinalizeDistribution()has minimal fixed costBatch size of 200 is optimal for gas efficiency
Complete Flow Gas Costs
1
~580,000
updateAttribution: 245k startDistribution: 165k processBatch: 120k finalizeDistribution: 49k
200
~16,433,000
updateAttribution: 9,872k startDistribution: 165k processBatch: 6,345k finalizeDistribution: 49k
Gas Cost Breakdown by Phase
Attribution Phase (ReplyCorp):
1 recipient: ~245,000 gas
200 recipients: ~9,872,000 gas
Per recipient: ~49,000 gas
Distribution Phase (Signer):
startDistribution: ~165,000 gas (fixed)
processBatch: ~30,000 gas per recipient
finalizeDistribution: ~49,000 gas (fixed)
Minimum (1 recipient): ~334,000 gas
200 recipients: ~6,559,000 gas
Multi-Batch Scenarios
For distributions with >200 recipients:
Each additional batch adds ~6,345,000 gas (for full 200-recipient batches)
Example: 500 recipients = 3 batches
Batch 0: ~6,345,000 gas
Batch 1: ~6,345,000 gas
Batch 2: ~1,586,000 gas (100 recipients)
Total batch processing: ~14,276,000 gas
Gas Optimization Notes
Batch Size: 200 recipients per batch is optimal (avoids gas limit while maximizing efficiency)
Attribution Updates: Can be expensive for large recipient lists - consider batching updates
Distribution: Fixed costs (~214,000 gas) + variable costs (~30,000 per recipient)
Finalization: Always required but minimal cost (~49,000 gas)
Running Gas Tests
This will run dedicated gas tests and output gas usage for each operation.
Test file: packages/contracts/test/FeeRouter.gas.test.ts
Security Audit Considerations
Known Limitations
Best-Effort Distribution: Failed transfers are not reverted - partial distributions possible
Batch Skipping: Batches can be skipped, converting funds to dust
No Native Token Support: ERC20 only
Bounded Lifetime: Not designed for unbounded conversion counts
No Fee-on-Transfer Support: Standard ERC20 only
Recommendations
Monitor Failed Transfers: Track
failedSoFarand handle off-chainProcess All Batches: Ensure all batches processed before finalization
Verify Attribution Hash: Always verify hash before starting distribution
Secure Owner Wallet: Owner controls significant functionality
Regular Dust Withdrawal: Withdraw accumulated dust periodically
License
SPDX-License-Identifier: MIT
References
Integration Guide: Integration Effort Guide
Last updated

