A multi-contract decentralized crowdfunding system built with Solidity and Foundry. The platform enables fundraisers to create and publish campaigns, funders to donate to verified campaigns, and a trusted service provider to oversee the entire lifecycle — including a timelocked disbursement flow that prevents premature or unauthorized fund withdrawals.
- Overview
- Architecture
- Contract Breakdown
- Campaign Lifecycle
- Disbursement Flow
- Deployed Contracts (Base Sepolia)
- Getting Started
- Running Tests
- Deployment
- CI/CD
- Known Limitations & Future Work
This platform has three distinct roles:
| Role | Description |
|---|---|
| Fundraiser | Registers on-chain, submits campaigns for review, and requests fund disbursements |
| Funder | Registers on-chain and donates ETH to verified, published campaigns |
| Service Provider | Deploys the contracts, verifies campaigns, approves disbursements after a mandatory timelock |
All role checks are enforced at the contract level. Unregistered addresses cannot donate or submit campaigns.
The system is split across five contracts with a shared interface layer to enable clean cross-contract communication:
IAccessControl.sol ← Interfaces for cross-contract role checks
Accesscontrol.sol ← Abstract base: role modifiers + service provider logic
Registeration.sol ← FundraiserRegisteration + FunderRegisteration
Fundraising_Campaign.sol ← Campaign submit / verify / publish lifecycle
Donation.sol ← Payable donations to active campaigns
Disbursement.sol ← Timelocked fund request + approval + disbursal
Each contract inherits from AccessControl, which wires up the registration contracts via their interfaces and exposes shared modifiers (onlyFunders, onlyFundRaisers, onlyServiceProvider, confirmDelay).
Abstract base contract inherited by all others. Stores references to the funder and fundraiser registries and exposes role-based modifiers. Modifiers delegate to internal _check functions to avoid the Solidity modifier stack-depth issue.
Two independent registration contracts in one file:
FundraiserRegisteration— stores name, email, country, and registration time for each fundraiser. Only the service provider can look up details.FunderRegisteration— same structure for funders. Both exposeisRegistered*view functions used byAccessControlfor modifier checks.
Manages the campaign proposal lifecycle:
submitCampaign(title, description, goalAmount, deadline, category)— callable only by registered fundraisers. Generates a unique campaign ID viakeccak256(fundraiser, timestamp).verifyCampaign(campaignId)— service provider only.publishCampaign(campaignId)— service provider only; requires prior verification.getCampaignDetails(campaignId)/getCampaignStatus(campaignId)— public view functions.
donate(campaignId)— payable, restricted to registered funders. Requires the campaign to be both verified and published. Tracks total donations per campaign and per donor.
Three-step disbursement with a mandatory 2-day timelock between request and approval:
requestDisbursement(campaignId, amount, purpose)— fundraiser submits a request.verifyDisbursement(requestId)— service provider marks request as verified.approveDisbursement(requestId)— service provider approves only after the 2-day delay has elapsed (confirmDelaymodifier).disburseFunds(requestId)— service provider releases the funds; campaign balance is decremented.
Fundraiser registers
│
▼
Fundraiser submits campaign ──► CampaignSubmitted event
│
▼
Service provider verifies ──► CampaignVerified event
│
▼
Service provider publishes ──► CampaignPublished event
│
▼
Funders donate (ETH)
│
▼
Fundraiser requests disbursement
│
▼
Service provider verifies request
│
(2-day timelock)
│
▼
Service provider approves ──► DisbursementApproved event
│
▼
Service provider disburses ──► Disbursed event
The disbursement flow enforces a separation between verification and approval, with a mandatory 2-day window between them. This gives stakeholders time to flag suspicious requests before funds move.
// Approval will revert if called before 2 days have elapsed since the request
modifier confirmDelay(uint256 requestId) {
require(IcheckTime(checktimeRegistry).checkTime(requestId), "Delay not over");
_;
}| Contract | Address |
|---|---|
| AccessControl | 0xfa7c94fcbd4deb69ed20a728328f94e21ebd1618 |
| FundraiserRegisteration | — (see Deployment script) |
| FunderRegisteration | 0xc89ab5e157f46e96d5343f3d7c1865752fe6da0b |
| Fundraising_Campaign | 0x7e9b1cdfc688d7a6762f7e69767a9274665f304f |
| Donation | 0xab45f755925bff1e3d6a0875b3739935ed3e3156 |
| Disbursement | 0x96c6803a071977ed9444c9da7538bd30c70fd22e |
Network: Base Sepolia (Chain ID: 84532)
- Foundry
- Git
git clone https://github.com/Ay-obami/Crowdfunding.git
cd Crowdfunding
forge installforge buildforge test -vvvThe test suite covers:
- Funder registration and duplicate registration revert
- Funder detail lookup (service provider gated)
- Fundraiser registration
- Fundraiser detail lookup
- Campaign submission and full detail retrieval (using recorded logs to extract the emitted campaign ID)
forge snapshotforge fmt --checkCopy .env.example to .env and fill in your RPC URL and private key:
SEPOLIA_RPC_URL=<your_base_sepolia_rpc_url>
PRIVATE_KEY=<your_private_key>Then run:
forge script script/Deployment.s.sol:deployment \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcastThe deployment script deploys all five contracts in dependency order and returns their addresses.
A GitHub Actions workflow runs on every push and pull request:
- Checkout with submodules
- Install Foundry (nightly)
forge fmt --check— enforces consistent formattingforge build --sizes— compiles and reports contract sizesforge test -vvv— runs the full test suite