A comprehensive guide to the system design, data model, and component interactions in the Fund-My-Cause decentralized crowdfunding platform.
Fund-My-Cause is a decentralized crowdfunding platform built on the Stellar network using Soroban smart contracts. The system enables creators to launch campaigns, accept contributions in XLM or custom tokens, and automatically release or refund funds based on campaign success.
The contract uses two types of Soroban storage:
Stores campaign metadata and configuration. Survives contract lifetime.
Storage Keys:
KEY_CREATOR(Address) - Campaign creator's Stellar addressKEY_TOKEN(Address) - Token address for contributionsKEY_GOAL(i128) - Funding goal in stroopsKEY_DEADLINE(u64) - Campaign deadline (Unix timestamp, seconds)KEY_TOTAL(i128) - Total amount raised in stroopsKEY_STATUS(Status) - Current campaign statusKEY_MIN(i128) - Minimum contribution amount in stroopsKEY_TITLE(String) - Campaign titleKEY_DESC(String) - Campaign descriptionKEY_SOCIAL(Vec) - Social media linksKEY_PLATFORM(PlatformConfig) - Optional platform fee configurationKEY_ADMIN(Address) - Admin address (same as creator)DataKey::ContributorCount(u32) - Number of unique contributorsDataKey::LargestContribution(i128) - Largest single contributionDataKey::AcceptedTokens(Vec) - Whitelist of accepted tokens
Stores per-contributor data with TTL management.
Storage Keys:
DataKey::Contribution(Address)(i128) - Contribution amount per contributorDataKey::ContributorPresence(Address)(bool) - Whether address has contributedKEY_CONTRIBS(Vec) - List of all contributor addresses
pub enum Status {
Active, // Campaign accepting contributions
Successful, // Deadline passed, goal reached
Refunded, // Deadline passed, goal not reached
Cancelled, // Creator cancelled campaign
Paused, // Campaign temporarily paused
}pub struct CampaignStats {
pub total_raised: i128, // Total raised in stroops
pub goal: i128, // Goal in stroops
pub progress_bps: u32, // Progress in basis points (0-10000)
pub contributor_count: u32, // Number of unique contributors
pub average_contribution: i128, // Average contribution in stroops
pub largest_contribution: i128, // Largest single contribution
}pub struct PlatformConfig {
pub address: Address, // Fee recipient address
pub fee_bps: u32, // Fee in basis points (0-10000)
}pub struct CampaignInfo {
pub creator: Address,
pub token: Address,
pub goal: i128,
pub deadline: u64,
pub min_contribution: i128,
pub title: String,
pub description: String,
pub status: Status,
pub has_platform_config: bool,
pub platform_fee_bps: u32,
pub platform_address: Address,
} ┌─────────────────────────────────┐
│ │
▼ │
┌──────────────┐ │
│ Active │◄───────────────────────┘
└──────┬───────┘ unpause()
│
┌────────────┼────────────┐
│ │ │
pause() deadline cancel_campaign()
│ passes │
│ │ │
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌──────────┐
│ Paused │ │ Deadline │ │Cancelled │
└────────┘ │ Reached? │ └──────────┘
│ └──────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────┐ ┌────────┐ │
│ │Success │ │Refunded│ │
│ └────────┘ └────────┘ │
│ │ │ │
└───────┴─────────┴─────────┘
withdraw() or refund_single()
| From | To | Condition | Function |
|---|---|---|---|
| Active | Paused | Admin calls pause() | pause() |
| Paused | Active | Admin calls unpause() | unpause() |
| Active | Cancelled | Creator calls cancel_campaign() | cancel_campaign() |
| Active | Successful | Deadline passed + goal reached + creator calls withdraw() | withdraw() |
| Active | Refunded | Deadline passed + goal not reached (implicit) | refund_single() |
| Cancelled | - | Contributors claim refunds | refund_single() |
| Refunded | - | Contributors claim refunds | refund_single() |
App
├── WalletProvider
│ ├── ThemeProvider
│ │ ├── Navbar
│ │ │ ├── ConnectButton
│ │ │ └── ThemeToggle
│ │ ├── CampaignList
│ │ │ ├── CampaignCard (multiple)
│ │ │ │ ├── ProgressBar
│ │ │ │ ├── CountdownTimer
│ │ │ │ └── PledgeButton
│ │ │ └── Pagination
│ │ ├── CampaignDetail
│ │ │ ├── CampaignHeader
│ │ │ ├── ProgressBar
│ │ │ ├── CountdownTimer
│ │ │ ├── ContributorList
│ │ │ ├── PledgeModal
│ │ │ └── WithdrawButton (if creator)
│ │ └── CreateCampaign
│ │ ├── FormInput
│ │ ├── DatePicker
│ │ └── SubmitButton
│ └── Toast (notifications)
- Purpose: Manages Freighter wallet connection and transaction signing
- State: address, isConnecting, isAutoConnecting, error, networkMismatch
- Methods: connect(), disconnect(), signTx()
- Features: Auto-restore from sessionStorage, network validation
- Purpose: Manages dark/light theme preference
- State: theme ("dark" or "light")
- Methods: toggleTheme()
- Features: Persists to localStorage, respects system preference
- Purpose: Fetches and manages campaign data
- State: info, loading, error
- Methods: refresh()
- Features: Auto-refetch on contractId change, cancellation support
1. User clicks "Pledge" button
↓
2. PledgeModal opens, user enters amount
↓
3. User clicks "Contribute"
↓
4. Frontend builds transaction XDR
- Method: contribute(contributor, amount, token)
- Caller: user's wallet address
↓
5. Frontend simulates transaction
- Estimates resource fees
- Detects contract errors early
- Returns prepared XDR
↓
6. Frontend requests wallet signature
- Calls Freighter signTransaction()
- User approves in wallet extension
↓
7. Frontend submits signed transaction
- Sends to Soroban RPC
- Polls for confirmation (max 20 attempts, 1.5s each)
↓
8. Contract executes
- Validates contribution amount
- Transfers tokens from contributor to contract
- Updates contributor's total
- Increments contributor count if first contribution
- Publishes "contributed" event
↓
9. Frontend receives confirmation
- Updates UI with new total raised
- Shows success toast
- Refreshes campaign stats
1. Creator clicks "Withdraw Funds"
↓
2. Frontend checks conditions
- Deadline passed?
- Goal reached?
- Creator authorized?
↓
3. Frontend builds transaction XDR
- Method: withdraw()
- Caller: creator's wallet address
↓
4. Frontend simulates and signs (same as contribution)
↓
5. Contract executes
- Validates deadline passed
- Validates goal reached
- Calculates platform fee (if configured)
- Transfers fee to platform address
- Transfers remaining to creator
- Sets status to Successful
↓
6. Frontend updates UI
- Shows withdrawal confirmation
- Updates campaign status
1. Contributor clicks "Claim Refund"
↓
2. Frontend checks conditions
- Campaign cancelled OR (deadline passed AND goal not reached)?
↓
3. Frontend builds transaction XDR
- Method: refund_single(contributor)
- Caller: contributor's wallet address
↓
4. Frontend simulates and signs
↓
5. Contract executes
- Validates refund eligibility
- Transfers refund amount to contributor
- Sets contributor's balance to 0
- Publishes "refunded" event
↓
6. Frontend updates UI
- Shows refund confirmation
- Updates contributor's balance
The Registry contract enables campaign discovery and management.
Registers a campaign contract ID in the registry.
Parameters:
campaign_id- Address of the crowdfund contract to register
Returns: Ok(()) on success
Side Effects:
- Adds campaign to registry list
- Publishes "registered" event
Returns a paginated list of registered campaign contract IDs.
Parameters:
offset- Starting index (0-based)limit- Maximum results (capped at 50)
Returns: Vector of campaign contract addresses
Returns the total number of registered campaigns.
Returns: Total count
campaigns(Vec) - List of all registered campaign contract IDscampaign_count(u32) - Total number of campaigns
-
Creator Trust: Creators are trusted to:
- Set reasonable campaign parameters
- Not abuse pause/unpause functionality
- Withdraw funds only when eligible
-
Contributor Trust: Contributors are trusted to:
- Provide valid wallet addresses
- Understand campaign terms before contributing
- Claim refunds when eligible
-
Platform Trust: Platform is trusted to:
- Set reasonable fee percentages (0-100%)
- Not modify contract code after deployment
- Maintain registry accuracy
-
Stellar Network Security
- Assumes Stellar consensus is secure
- Assumes ledger timestamps are accurate
- Assumes token contracts are properly implemented
-
Wallet Security
- Assumes Freighter wallet is secure
- Assumes user private keys are protected
- Assumes user approves transactions intentionally
-
Contract Immutability
- Contract code cannot be modified after deployment
- Contract address is permanent
- Storage is persistent across invocations
-
Authorization
- All state-changing operations require caller authorization
- Creator must authorize initialization, withdrawal, metadata updates
- Contributors must authorize contributions and refunds
-
Validation
- Goal must be > 0
- Deadline must be in future
- Minimum contribution must be >= 0
- Platform fee must be <= 10,000 bps (100%)
- Contribution amount must be >= minimum
-
Pull-Based Refunds
- Each contributor claims their own refund
- Avoids single-transaction failure at scale
- Prevents gas limit issues with many contributors
-
Token Whitelist
- Optional whitelist of accepted tokens
- Prevents accidental contributions in wrong token
- Falls back to default token if no whitelist
-
Platform Fee Deduction
- Fee calculated as:
(total_raised * fee_bps) / 10_000 - Deducted before creator payout
- Prevents fee manipulation
- Fee calculated as:
-
Ledger Entry Expiration
- Persistent storage entries have TTL
- Contributions may expire if not extended
- Mitigation: Contract extends TTL on each contribution
-
Token Contract Failure
- If token contract is malicious, transfers may fail
- Mitigation: Use well-known, audited token contracts
-
Deadline Manipulation
- Creator can extend deadline indefinitely
- Mitigation: Governance or time limits (future enhancement)
-
Platform Fee Abuse
- Platform could set very high fees
- Mitigation: Transparent fee configuration, community oversight
- Deploy Crowdfund contract
- Deploy Registry contract
- Initialize Crowdfund with test parameters
- Register campaign in Registry
- Configure frontend with contract IDs
- Audit contract code
- Deploy Crowdfund contract
- Deploy Registry contract
- Initialize with production parameters
- Verify contracts on Stellar Expert
- Update frontend environment variables
- Monitor contract events
- Contribution: ~5,000-10,000 stroops
- Withdrawal: ~3,000-5,000 stroops
- Refund: ~2,000-4,000 stroops
- Metadata Update: ~1,000-2,000 stroops
- Contributor Limit: No hard limit, but pagination recommended
- Campaign Limit: Registry can handle thousands of campaigns
- Storage: Instance storage is limited; persistent storage scales better
- Use pagination for large contributor lists
- Cache campaign stats on frontend
- Batch refund claims when possible
- Use persistent storage for contributor data
- Multi-Token Support: Accept multiple tokens simultaneously
- Milestone-Based Funding: Release funds at milestones
- Governance: DAO-based campaign approval
- Reputation System: Track creator/contributor history
- Escrow Service: Third-party fund management
- Insurance: Protect against creator fraud
- Secondary Market: Trade campaign tokens
- Staking: Earn rewards for participation