Decentralized Crowdfunding Platform for Climate & Social Impact Projects
- Overview
- Key Features
- Architecture
- Getting Started
- API Documentation
- Smart Contract Endpoints
- Usage Examples
- Technologies
- Testing
- Contributing
- License
EcoVault Finance is a next-generation decentralized crowdfunding platform built on Ethereum, designed specifically for climate action and social impact projects. By leveraging the EIP-2535 Diamond Proxy Standard, the platform achieves unparalleled modularity, upgradeability, and gas efficiency.
Traditional crowdfunding platforms suffer from:
- ❌ High platform fees (5-10%)
- ❌ Geographic restrictions
- ❌ Lack of transparency in fund usage
- ❌ Limited donor governance
- ❌ No verification of project completion
EcoVault Finance provides:
- ✅ Zero platform fees (only gas costs)
- ✅ Global accessibility (permissionless, borderless)
- ✅ Milestone-based funding (pay-as-you-deliver)
- ✅ DAO governance (donors vote on fund releases)
- ✅ Oracle-verified evidence (proof of work completion)
- ✅ Decentralized identity (KYC/KYB via ONCHAINID)
- Upgradeable without redeploying the entire system
- Add/remove features seamlessly
- Gas-efficient function routing
- Single contract address for all interactions
- KYC (Know Your Customer): For individual donors
- KYB (Know Your Business): For campaign creators
- ERC-734/ERC-735 compliant
- Privacy-preserving claims system
- Trusted issuer verification
- Funds locked until milestones are completed
- Evidence submission required for each milestone
- DAO voting on milestone approvals
- Automatic refunds if campaign fails
- Proportional voting power based on donations
- On-chain voting for milestone approvals
- Community-driven decision making
- Tier-based membership system
- Automatic campaign finalization at deadline
- Scheduled milestone reviews
- Oracle-based evidence verification
- AI-powered proof of work validation
- OpenZeppelin battle-tested contracts
- Reentrancy protection
- Access control modifiers
- Pull-over-push payment pattern
- Native ETH donations
- Any ERC-20 token support
- Configurable per campaign
- Automatic token handling
┌─────────────────────────────────────────────────────────────┐
│ Diamond Proxy (EIP-2535) │
│ Single Entry Point Contract │
└─────────────────────┬───────────────────────────────────────┘
│
┌────────────┼────────────┬─────────────┐
│ │ │ │
┌────▼────┐ ┌───▼────┐ ┌───▼────┐ ┌────▼─────┐
│Identity │ │Campaign│ │Campaign│ │Management│
│ Facet │ │Factory │ │Instance│ │ Facet │
└────┬────┘ └───┬────┘ └───┬────┘ └────┬─────┘
│ │ │ │
└───────────┴────────────┴─────────────┘
│
┌────────────┴────────────┐
│ │
┌────▼─────┐ ┌─────▼────┐
│ONCHAINID │ │Chainlink │
│ Identity │ │ Oracles │
└──────────┘ └──────────┘
Core Contracts:
- Diamond.sol: Main proxy contract (all interactions start here)
- LibAppStorage.sol: Shared storage library (state management)
- CampaignInstance.sol: Individual campaign logic (minimal proxies)
Facets (Pluggable Modules):
- IdentityFacet.sol: User registration & KYC/KYB verification
- CampaignFactoryFacet.sol: Campaign deployment
- CampaignManagementFacet.sol: Cross-campaign coordination, DAO voting
- DiamondCutFacet.sol: System upgrades & facet management
Key Design Patterns:
- Diamond Proxy: Modular upgradeable architecture
- Minimal Proxy (EIP-1167): Gas-efficient campaign deployment
- Diamond Storage: Conflict-free shared state
- Pull Payment: Secure fund withdrawals
- Git: Version control
- Foundry: Solidity development toolkit
curl -L https://foundry.paradigm.xyz | bash foundryup - Node.js (optional): For frontend integration
# 1. Clone the repository
git clone https://github.com/EcoVault-Finance/core-contracts.git
cd core-contracts
# 2. Install dependencies
forge install
# 3. Build contracts
forge build
# 4. Run tests
forge test -vvvCreate a .env file in the root directory:
# Network RPC URLs
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY
# Deployer Wallet
DEPLOYER_PRIVATE_KEY=0xYourPrivateKey
# Verification
ETHERSCAN_API_KEY=YOUR_ETHERSCAN_API_KEY
# Chainlink (Sepolia Testnet)
LINK_TOKEN_ADDRESS=0x779877A7B0D9E8603169DdbD7836e478b4624789
AUTOMATION_REGISTRAR_ADDRESS=0x9806cf6fBc89aBF286e8140C42174B94DfC62AAd
AUTOMATION_REGISTRY_ADDRESS=0x86EFBD0b6736Bed994962f9797049422A3A8E8Ad
# Identity Factory
IDENTITY_FACTORY_ADDRESS=0xYourDeployedFactoryAddress# Deploy to local testnet (Anvil)
anvil # In separate terminal
forge script script/DeployDiamond.s.sol --rpc-url http://localhost:8545 --broadcast
# Deploy to Sepolia
forge script script/DeployDiamond.s.sol \
--rpc-url $SEPOLIA_RPC_URL \
--private-key $DEPLOYER_PRIVATE_KEY \
--broadcast \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEYAll interactions occur through the Diamond Proxy Contract:
Sepolia Testnet: 0xYourDiamondProxyAddress
Mainnet: 0xYourMainnetDiamondAddress
- No API keys required (blockchain-based authentication)
- Wallet signatures authenticate all transactions
- ONCHAINID claims authorize privileged operations
- Determined by blockchain block time (~12 seconds on Ethereum)
- Gas limits per block (~30M gas on mainnet)
- No external rate limiting
Description: Registers a new user by deploying an ONCHAINID identity contract.
Request:
// No parameters - msg.sender is the user
registerUser()Response:
address identityContract // Address of deployed identity contractEvents Emitted:
event IdentityRegistered(
address indexed user,
address indexed identityContract
)Errors:
IdentityFactoryNotSet: Identity factory not configuredUserAlreadyRegistered: User already has an identityIdentityCreationFailed: Factory call failedInvalidIdentityContract: Deployed address invalid
Example (cast):
cast send $DIAMOND_ADDRESS "registerUser()" \
--private-key $USER_PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URLDescription: Retrieves comprehensive identity status including KYC/KYB verification.
Request:
function getIdentityStatus(address user) external view returns (
address identityContract,
bool hasKYB,
uint64 kybExpiry,
address kybIssuer,
bool hasKYC,
uint64 kycExpiry,
address kycIssuer,
bool isVerified,
uint64 linkedAt
)Response:
{
"identityContract": "0x1234...5678",
"hasKYB": true,
"kybExpiry": 1735689600,
"kybIssuer": "0xABCD...EF00",
"hasKYC": true,
"kycExpiry": 1735689600,
"kycIssuer": "0xABCD...EF00",
"isVerified": false,
"linkedAt": 1672531200
}Example:
cast call $DIAMOND_ADDRESS "getIdentityStatus(address)" $USER_ADDRESS \
--rpc-url $SEPOLIA_RPC_URLDescription: Checks if a user has valid KYB (Know Your Business) verification.
Request:
function isKYBVerified(address user) external view returns (
bool hasKYB,
uint64 expiry,
address issuer
)Response:
{
"hasKYB": true,
"expiry": 1735689600,
"issuer": "0xABCD...EF00"
}Use Case: Called by factory before allowing campaign creation.
Description: Checks if a user has valid KYC (Know Your Customer) verification.
Request:
function isKYCVerified(address user) external view returns (
bool hasKYC,
uint64 expiry,
address issuer
)Response:
{
"hasKYC": true,
"expiry": 1735689600,
"issuer": "0xABCD...EF00"
}Use Case: Called before accepting donations.
Description: Deploys a new campaign instance for a KYB-verified user.
Request:
function createNewCampaign(
string calldata title,
string calldata description,
uint256 goalAmount,
address token,
uint64 duration
) external returns (
address campaignAddress,
uint256 campaignId
)Parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
title |
string | Campaign title (max 200 chars) | "Clean Water for Rural Africa" |
description |
string | Detailed description | "This project aims to..." |
goalAmount |
uint256 | Funding goal in token units | 1000000000000000000000 (1000 ETH) |
token |
address | ERC20 token or address(0) for ETH |
0x0000...0000 or 0x779877... |
duration |
uint64 | Campaign duration in seconds | 2592000 (30 days) |
Response:
{
"campaignAddress": "0x5678...1234",
"campaignId": 42
}Events Emitted:
event CampaignRegistered(
uint256 indexed campaignId,
address indexed campaignAddress,
address indexed creator
)
event CampaignInitialized(
address indexed creator,
string title,
uint256 goalAmount,
uint64 deadline
)Errors:
Factory_NotKYBVerified: Creator lacks KYB verificationFactory_NoIdentityContract: Creator has no identityFactory_KYBExpired: KYB verification expiredFactory_ZeroGoal: Goal amount is zeroFactory_InvalidDuration: Duration < 1 day (86400 seconds)Factory_InvalidTitle: Title is emptyFactory_ImplementationNotSet: Campaign logic not set by adminFactory_ProxyDeploymentFailed: Minimal proxy deployment failedFactory_InitializationFailed: Campaign initialization failed
Example:
cast send $DIAMOND_ADDRESS "createNewCampaign(string,string,uint256,address,uint64)" \
"Solar Farm Initiative" \
"Building 100MW solar farm in Sub-Saharan Africa" \
"1000000000000000000000" \
"0x0000000000000000000000000000000000000000" \
"2592000" \
--private-key $CREATOR_PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URLDescription: Donates to a campaign (called on CampaignInstance, not Diamond).
Request:
function donate(uint256 amount) external payableParameters:
| Parameter | Type | Description | Notes |
|---|---|---|---|
amount |
uint256 | Donation amount | Must match msg.value for native token |
Response:
// No return value - check eventsEvents Emitted:
event DonationReceived(
address indexed donor,
uint256 amount,
uint256 newTotalRaised
)
event DonorRegistered(
address indexed donor,
address indexed identity,
uint256 totalDonations
)
event VotingPowerUpdated(
address indexed donor,
uint256 oldPower,
uint256 newPower
)Errors:
CampaignInstance_NotInitialized: Campaign not initializedCampaignInstance_CampaignNotActive: Campaign not activeCampaignInstance_CampaignEnded: Deadline passedCampaignInstance_InvalidAmount: Amount is zeroCampaignInstance_NotKYCVerified: Donor lacks KYCCampaignInstance_DonationExceedsMaximum: Exceeds max limitCampaignInstance_DonationBelowMinimum: Below min limitCampaignInstance_NativeTokenMismatch:msg.value≠amountCampaignInstance_NoNativeTokensExpected: Sent ETH to ERC20 campaign
Example (Native ETH):
cast send $CAMPAIGN_ADDRESS "donate(uint256)" \
"1000000000000000000" \
--value 1ether \
--private-key $DONOR_PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URLExample (ERC20):
# 1. Approve campaign to spend tokens
cast send $TOKEN_ADDRESS "approve(address,uint256)" \
$CAMPAIGN_ADDRESS \
"10000000000000000000" \
--private-key $DONOR_PRIVATE_KEY
# 2. Donate tokens
cast send $CAMPAIGN_ADDRESS "donate(uint256)" \
"10000000000000000000" \
--private-key $DONOR_PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URLDescription: Creates a new milestone for a campaign (creator only).
Request:
function createMilestone(
string calldata milestoneTitle,
string calldata milestoneDescription,
uint32 percentage,
uint64 deadline,
MilestoneType mType
) external returns (uint32 milestoneId)Parameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
milestoneTitle |
string | Milestone title | "Phase 1: Solar Panel Installation" |
milestoneDescription |
string | Detailed description | "Install 500 solar panels..." |
percentage |
uint32 | % of goal (basis points) | 2500 (25.00%) |
deadline |
uint64 | Completion deadline (Unix) | 1735689600 |
mType |
enum | Milestone type | 0 (FUNDING_PROPOSAL) |
Milestone Types:
enum MilestoneType {
FUNDING_PROPOSAL, // 0: Initial funding request
WORK_DELIVERY, // 1: Evidence of completed work
FINANCIAL_REPORT, // 2: Spending transparency
IMPACT_MEASUREMENT // 3: Social/environmental impact
}Response:
{
"milestoneId": 1
}Events Emitted:
event MilestoneCreated(
uint32 indexed milestoneId,
string title,
uint32 percentage,
uint64 deadline
)Errors:
CampaignInstance_NotInitialized: Campaign not initializedCampaignInstance_NotCreator: Caller not campaign creatorCampaignInstance_CampaignNotActive: Campaign not activeCampaignInstance_InvalidPercentage: Percentage = 0 or > 10000CampaignInstance_CampaignEnded: Deadline in the past
Example:
cast send $CAMPAIGN_ADDRESS "createMilestone(string,string,uint32,uint64,uint8)" \
"Phase 1: Land Acquisition" \
"Secure 100 hectares of land for solar farm" \
"2500" \
"1735689600" \
"0" \
--private-key $CREATOR_PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URLDescription: Submits evidence for milestone completion (triggers oracle verification).
Request:
function submitEvidence(
uint32 milestoneId,
string calldata evidenceURI
) externalParameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
milestoneId |
uint32 | Milestone ID | 1 |
evidenceURI |
string | IPFS/Arweave link | ipfs://QmX...abc |
Events Emitted:
event EvidenceSubmitted(
uint32 indexed milestoneId,
string evidenceURI,
uint64 timestamp
)
event OracleVerificationRequested(
uint256 indexed campaignId,
uint32 indexed milestoneId,
address indexed oracle,
bytes32 requestId
)Errors:
CampaignInstance_NotInitialized: Campaign not initializedCampaignInstance_NotCreator: Caller not creatorCampaignInstance_CampaignNotActive: Campaign not activeCampaignInstance_MilestoneNotFound: Invalid milestone IDCampaignInstance_InvalidMilestoneStatus: Milestone already processed
Example:
cast send $CAMPAIGN_ADDRESS "submitEvidence(uint32,string)" \
"1" \
"ipfs://QmXa1b2c3d4e5f6g7h8i9j0k..." \
--private-key $CREATOR_PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URLDescription: Finalizes campaign after deadline (anyone can call, typically Chainlink Automation).
Request:
function finalizeCampaign() externalResponse:
// No return value - check eventsEvents Emitted:
event CampaignStateUpdated(
CampaignState oldState,
CampaignState newState
)
event CampaignSuccessful(
uint256 totalRaised,
uint256 goalAmount
)
// OR
event CampaignFailed(
uint256 totalRaised,
uint256 goalAmount
)Logic:
- If
totalRaised >= goalAmount→CampaignState.Successful - If
totalRaised < goalAmount→CampaignState.Failed(refunds enabled)
Errors:
CampaignInstance_NotInitialized: Campaign not initializedCampaignInstance_DeadlineNotReached: Deadline not yet passedCampaignInstance_AlreadyFinalized: Campaign already finalized
Example:
cast send $CAMPAIGN_ADDRESS "finalizeCampaign()" \
--rpc-url $SEPOLIA_RPC_URLDescription: Claims refund if campaign failed (pull payment pattern).
Request:
function claimRefund() externalResponse:
// No return value - check eventsEvents Emitted:
event RefundClaimed(
address indexed donor,
uint256 amount
)Errors:
CampaignInstance_NotInitialized: Campaign not initializedCampaignInstance_RefundNotAvailable: Campaign not in Failed stateCampaignInstance_NoContribution: Caller has no contribution
Example:
cast send $CAMPAIGN_ADDRESS "claimRefund()" \
--private-key $DONOR_PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URLDescription: Batch refunds multiple donors (creator only, gas optimization).
Request:
function batchRefund(address[] calldata recipients) externalParameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
recipients |
address[] | Array of donor addresses | [0xAlice, 0xBob, 0xCharlie] |
Response:
// No return value - check events per recipientEvents Emitted:
event RefundClaimed(
address indexed donor,
uint256 amount
)
// Emitted once per successful refundErrors:
CampaignInstance_NotInitialized: Campaign not initializedCampaignInstance_NotCreator: Caller not creatorCampaignInstance_RefundNotAvailable: Campaign not Failed
Example:
cast send $CAMPAIGN_ADDRESS "batchRefund(address[])" \
"[0x1111...,0x2222...,0x3333...]" \
--private-key $CREATOR_PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URLDescription: Submits a vote for milestone approval (DAO members only).
Request:
function submitVote(
uint256 campaignId,
uint32 milestoneId,
bool approve
) externalParameters:
| Parameter | Type | Description | Example |
|---|---|---|---|
campaignId |
uint256 | Campaign ID | 42 |
milestoneId |
uint32 | Milestone ID | 1 |
approve |
bool | Approve or reject | true |
Response:
// No return value - check eventsEvents Emitted:
event DAOVoteRegistered(
uint256 indexed campaignId,
uint32 indexed milestoneId,
address indexed voter,
uint256 votingPower,
bool approved
)Voting Power Calculation:
votingPower = totalDonations[voter] // Proportional to all donationsErrors:
Coordinator_NoIdentityContract: Voter has no identityCoordinator_NotKYBVerified: Voter not KYB verifiedCoordinator_KYBExpired: Voter's KYB expiredCoordinator_CampaignNotFound: Invalid campaign IDCoordinator_CampaignNotActive: Campaign not activeCoordinator_AlreadyVoted: Voter already voted on this milestone
Example:
cast send $DIAMOND_ADDRESS "submitVote(uint256,uint32,bool)" \
"42" \
"1" \
"true" \
--private-key $DONOR_PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URLDescription: Aggregates votes and submits result to campaign (governance only).
Request:
function aggregateVotes(
uint256 campaignId,
uint32 milestoneId
) externalResponse:
// No return value - check eventsEvents Emitted:
event DAOVoteAggregated(
uint256 indexed campaignId,
uint32 indexed milestoneId,
uint256 totalApproval,
uint256 totalVotes,
bool approved
)Approval Logic:
approvalPercentage = (approvalVotes * 10000) / totalVotes
approved = approvalPercentage > 5000 // > 50%Errors:
Coordinator_NotGovernance: Caller not governance addressCoordinator_VotingNotComplete: No votes submitted
Example:
cast send $DIAMOND_ADDRESS "aggregateVotes(uint256,uint32)" \
"42" \
"1" \
--private-key $GOVERNANCE_PRIVATE_KEY \
--rpc-url $SEPOLIA_RPC_URLDescription: Returns donor's total voting power across all campaigns.
Request:
function getVotingPower(address donor) external view returns (uint256)Response:
{
"votingPower": "5000000000000000000000"
}Example:
cast call $DIAMOND_ADDRESS "getVotingPower(address)" $DONOR_ADDRESS \
--rpc-url $SEPOLIA_RPC_URLDescription: Returns donor's total donations across all campaigns.
Request:
function getTotalDonations(address donor) external view returns (uint256)Response:
{
"totalDonations": "5000000000000000000000"
}Description: Returns donor's community tier.
Request:
function getDonorTier(address donor) external view returns (uint32)Response:
{
"tier": 3
}Tier Levels:
| Tier | Name | Total Donations | Benefits |
|---|---|---|---|
| 0 | None | < 10 ETH | Basic access |
| 1 | Bronze | ≥ 10 ETH | Priority support |
| 2 | Silver | ≥ 100 ETH | Exclusive events |
| 3 | Gold | ≥ 1,000 ETH | Governance role |
| 4 | Platinum | ≥ 10,000 ETH | Advisory board |
Description: Returns campaign address by ID.
Request:
function getCampaignAddress(uint256 campaignId) external view returns (address)Response:
{
"campaignAddress": "0x5678...1234"
}#!/bin/bash
# Configuration
DIAMOND="0xYourDiamondAddress"
CREATOR_KEY="0xCreatorPrivateKey"
DONOR_KEY="0xDonorPrivateKey"
RPC="$SEPOLIA_RPC_URL"
# ========================================
# Step 1: Register Creator Identity
# ========================================
echo "Registering creator identity..."
cast send $DIAMOND "registerUser()" \
--private-key $CREATOR_KEY \
--rpc-url $RPC
# ========================================
# Step 2: Admin Verifies KYB (Off-chain)
# ========================================
# Trusted issuer adds KYB claim to creator's identity contract
# (This happens off-chain via the claim issuer's interface)
# ========================================
# Step 3: Create Campaign
# ========================================
echo "Creating campaign..."
CAMPAIGN_TX=$(cast send $DIAMOND "createNewCampaign(string,string,uint256,address,uint64)" \
"Ocean Cleanup Initiative" \
"Remove 10,000 tons of plastic from Pacific Ocean" \
"1000000000000000000000" \
"0x0000000000000000000000000000000000000000" \
"2592000" \
--private-key $CREATOR_KEY \
--rpc-url $RPC \
--json)
# Extract campaign address from logs
CAMPAIGN=$(echo $CAMPAIGN_TX | jq -r '.logs[0].topics[2]')
echo "Campaign deployed at: $CAMPAIGN"
# ========================================
# Step 4: Create Milestones
# ========================================
echo "Creating milestones..."
# Milestone 1: Equipment Purchase (25%)
cast send $CAMPAIGN "createMilestone(string,string,uint32,uint64,uint8)" \
"Equipment Procurement" \
"Purchase ocean cleanup vessels and equipment" \
"2500" \
"$(date -d '+30 days' +%s)" \
"0" \
--private-key $CREATOR_KEY \
--rpc-url $RPC
# Milestone 2: Operations (50%)
cast send $CAMPAIGN "createMilestone(string,string,uint32,uint64,uint8)" \
"Cleanup Operations" \
"6-month ocean cleanup operations" \
"5000" \
"$(date -d '+180 days' +%s)" \
"1" \
--private-key $CREATOR_KEY \
--rpc-url $RPC
# Milestone 3: Impact Report (25%)
cast send $CAMPAIGN "createMilestone(string,string,uint32,uint64,uint8)" \
"Impact Measurement" \
"Third-party verified impact assessment" \
"2500" \
"$(date -d '+210 days' +%s)" \
"3" \
--private-key $CREATOR_KEY \
--rpc-url $RPC
# ========================================
# Step 5: Donor Registration & Donation
# ========================================
echo "Registering donor..."
cast send $DIAMOND "registerUser()" \
--private-key $DONOR_KEY \
--rpc-url $RPC
# Wait for KYC verification (off-chain)
sleep 5
echo "Making donation..."
cast send $CAMPAIGN "donate(uint256)" \
"100000000000000000000" \
--value 100ether \
--private-key $DONOR_KEY \
--rpc-url $RPC
# ========================================
# Step 6: Submit Evidence for Milestone 1
# ========================================
echo "Submitting evidence..."
cast send $CAMPAIGN "submitEvidence(uint32,string)" \
"1" \
"ipfs://QmEquipmentPurchaseReceipts..." \
--private-key $CREATOR_KEY \
--rpc-url $RPC
# ========================================
# Step 7: DAO Voting
# ========================================
echo "Voting on milestone..."
cast send $DIAMOND "submitVote(uint256,uint32,bool)" \
"$(cast call $DIAMOND 'getCampaignAddress(uint256)' $CAMPAIGN)" \
"1" \
"true" \
--private-key $DONOR_KEY \
--rpc-url $RPC
# ========================================
# Step 8: Aggregate Votes (Governance)
# ========================================
echo "Aggregating votes..."
cast send $DIAMOND "aggregateVotes(uint256,uint32)" \
"$(cast call $DIAMOND 'getCampaignAddress(uint256)' $CAMPAIGN)" \
"1" \
--private-key $GOVERNANCE_KEY \
--rpc-url $RPC
# ========================================
# Step 9: Withdraw Milestone Funds
# ========================================
echo "Withdrawing milestone funds..."
cast send $CAMPAIGN "withdrawMilestoneFunds(uint32)" \
"1" \
--private-key $CREATOR_KEY \
--rpc-url $RPC
echo "Campaign lifecycle complete!"import { ethers } from 'ethers';
// Contract ABIs
import DiamondABI from './abis/Diamond.json';
import CampaignInstanceABI from './abis/CampaignInstance.json';
// Configuration
const DIAMOND_ADDRESS = '0xYourDiamondAddress';
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
// Initialize contracts
const diamond = new ethers.Contract(DIAMOND_ADDRESS, DiamondABI, signer);
// ========================================
// User Registration
// ========================================
async function registerUser() {
try {
const tx = await diamond.registerUser();
await tx.wait();
console.log('User registered successfully');
return tx;
} catch (error) {
console.error('Registration failed:', error);
throw error;
}
}
// ========================================
// Check KYB Status
// ========================================
async function checkKYBStatus(userAddress) {
try {
const [hasKYB, expiry, issuer] = await diamond.isKYBVerified(userAddress);
return {
verified: hasKYB,
expiresAt: new Date(expiry * 1000),
issuer: issuer,
isExpired: expiry > 0 && Date.now() / 1000 > expiry
};
} catch (error) {
console.error('KYB check failed:', error);
return { verified: false };
}
}
// ========================================
// Create Campaign
// ========================================
async function createCampaign(campaignData) {
const { title, description, goalAmount, token, durationDays } = campaignData;
try {
// Convert duration to seconds
const duration = durationDays * 24 * 60 * 60;
// Create campaign
const tx = await diamond.createNewCampaign(
title,
description,
ethers.utils.parseEther(goalAmount),
token || ethers.constants.AddressZero, // address(0) for ETH
duration
);
const receipt = await tx.wait();
// Parse CampaignRegistered event
const event = receipt.events?.find(e => e.event === 'CampaignRegistered');
const campaignId = event?.args?.campaignId;
const campaignAddress = event?.args?.campaignAddress;
console.log(`Campaign created: ID=${campaignId}, Address=${campaignAddress}`);
return { campaignId, campaignAddress, receipt };
} catch (error) {
console.error('Campaign creation failed:', error);
throw error;
}
}
// ========================================
// Donate to Campaign
// ========================================
async function donate(campaignAddress, amount, isNativeToken = true) {
const campaign = new ethers.Contract(campaignAddress, CampaignInstanceABI, signer);
try {
const amountWei = ethers.utils.parseEther(amount);
let tx;
if (isNativeToken) {
// Native ETH donation
tx = await campaign.donate(amountWei, { value: amountWei });
} else {
// ERC20 donation (requires prior approval)
tx = await campaign.donate(amountWei);
}
const receipt = await tx.wait();
console.log('Donation successful');
return receipt;
} catch (error) {
console.error('Donation failed:', error);
throw error;
}
}
// ========================================
// Create Milestone
// ========================================
async function createMilestone(campaignAddress, milestoneData) {
const campaign = new ethers.Contract(campaignAddress, CampaignInstanceABI, signer);
const { title, description, percentage, deadlineDays, type } = milestoneData;
try {
// Calculate deadline timestamp
const deadline = Math.floor(Date.now() / 1000) + (deadlineDays * 24 * 60 * 60);
// Convert percentage to basis points (25% = 2500)
const basisPoints = percentage * 100;
const tx = await campaign.createMilestone(
title,
description,
basisPoints,
deadline,
type // 0=FUNDING_PROPOSAL, 1=WORK_DELIVERY, 2=FINANCIAL_REPORT, 3=IMPACT_MEASUREMENT
);
const receipt = await tx.wait();
// Parse MilestoneCreated event
const event = receipt.events?.find(e => e.event === 'MilestoneCreated');
const milestoneId = event?.args?.milestoneId;
console.log(`Milestone created: ID=${milestoneId}`);
return { milestoneId, receipt };
} catch (error) {
console.error('Milestone creation failed:', error);
throw error;
}
}
// ========================================
// Submit Evidence
// ========================================
async function submitEvidence(campaignAddress, milestoneId, evidenceURI) {
const campaign = new ethers.Contract(campaignAddress, CampaignInstanceABI, signer);
try {
const tx = await campaign.submitEvidence(milestoneId, evidenceURI);
const receipt = await tx.wait();
console.log('Evidence submitted successfully');
return receipt;
} catch (error) {
console.error('Evidence submission failed:', error);
throw error;
}
}
// ========================================
// Vote on Milestone
// ========================================
async function voteOnMilestone(campaignId, milestoneId, approve) {
try {
const tx = await diamond.submitVote(campaignId, milestoneId, approve);
const receipt = await tx.wait();
console.log(`Vote submitted: ${approve ? 'Approved' : 'Rejected'}`);
return receipt;
} catch (error) {
console.error('Voting failed:', error);
throw error;
}
}
// ========================================
// Get Donor Info
// ========================================
async function getDonorInfo(donorAddress) {
try {
const [votingPower, totalDonations, tier] = await Promise.all([
diamond.getVotingPower(donorAddress),
diamond.getTotalDonations(donorAddress),
diamond.getDonorTier(donorAddress)
]);
return {
votingPower: ethers.utils.formatEther(votingPower),
totalDonations: ethers.utils.formatEther(totalDonations),
tier: tier.toNumber(),
tierName: getTierName(tier.toNumber())
};
} catch (error) {
console.error('Failed to get donor info:', error);
throw error;
}
}
function getTierName(tier) {
const tiers = ['None', 'Bronze', 'Silver', 'Gold', 'Platinum'];
return tiers[tier] || 'Unknown';
}
// ========================================
// React Hook Example
// ========================================
import { useState, useEffect } from 'react';
function useCampaignData(campaignAddress) {
const [campaign, setCampaign] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchCampaignData() {
try {
const contract = new ethers.Contract(
campaignAddress,
CampaignInstanceABI,
provider
);
const [
creator,
title,
description,
goalAmount,
totalRaised,
deadline,
state,
tokenAddress
] = await Promise.all([
contract.getCreator(),
contract.title(),
contract.description(),
contract.getGoalAmount(),
contract.getTotalRaised(),
contract.getDeadline(),
contract.getCampaignState(),
contract.getTokenAddress()
]);
setCampaign({
address: campaignAddress,
creator,
title,
description,
goalAmount: ethers.utils.formatEther(goalAmount),
totalRaised: ethers.utils.formatEther(totalRaised),
deadline: new Date(deadline * 1000),
state: ['Active', 'Paused', 'Successful', 'Failed', 'Cancelled'][state],
isNativeToken: tokenAddress === ethers.constants.AddressZero,
tokenAddress,
progress: (totalRaised / goalAmount) * 100
});
} catch (error) {
console.error('Failed to fetch campaign data:', error);
} finally {
setLoading(false);
}
}
fetchCampaignData();
}, [campaignAddress]);
return { campaign, loading };
}
// Export functions
export {
registerUser,
checkKYBStatus,
createCampaign,
donate,
createMilestone,
submitEvidence,
voteOnMilestone,
getDonorInfo,
useCampaignData
};| Technology | Purpose | Version |
|---|---|---|
| Solidity | Smart contract language | 0.8.20+ |
| Foundry | Development framework | Latest |
| EIP-2535 Diamond | Modular architecture | Standard |
| OpenZeppelin | Security libraries | 5.0.0 |
| Chainlink Automation | Automated operations | v2.0 |
| Chainlink Oracles | External data feeds | Latest |
| ONCHAINID | Decentralized identity | ERC-734/735 |
| ERC-1167 | Minimal proxy clones | Standard |
[dependencies]
# OpenZeppelin Contracts
forge-std = { git = "https://github.com/foundry-rs/forge-std", tag = "v1.7.0" }
openzeppelin-contracts = { git = "https://github.com/OpenZeppelin/openzeppelin-contracts", tag = "v5.0.0" }
# Chainlink
chainlink = { git = "https://github.com/smartcontractkit/chainlink", tag = "v2.0.0" }
# ONCHAINID
onchainid = { git = "https://github.com/onchain-id/solidity", tag = "v4.0.0" }# Run all tests
forge test
# Run with verbosity (show logs)
forge test -vvv
# Run specific test file
forge test --match-path test/CampaignInstance.t.sol
# Run specific test function
forge test --match-test testDonation
# Run with gas reporting
forge test --gas-report
# Run with coverage
forge coverage
# Generate coverage report (HTML)
forge coverage --report lcov
genhtml lcov.info -o coveragetest/
├── unit/
│ ├── Diamond.t.sol # Diamond proxy tests
│ ├── IdentityFacet.t.sol # Identity management tests
│ ├── CampaignFactory.t.sol # Campaign creation tests
│ └── CampaignInstance.t.sol # Campaign lifecycle tests
├── integration/
│ ├── EndToEnd.t.sol # Full campaign lifecycle
│ ├── DAOVoting.t.sol # Governance tests
│ └── OracleIntegration.t.sol # Oracle verification tests
├── fork/
│ └── SepoliaFork.t.sol # Mainnet fork tests
└── helpers/
└── TestHelpers.sol # Shared test utilities
// test/unit/CampaignInstance.t.sol
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/CampaignInstance.sol";
contract CampaignInstanceTest is Test {
CampaignInstance campaign;
address creator = address(0x1);
address donor = address(0x2);
function setUp() public {
campaign = new CampaignInstance(1, address(this));
vm.prank(creator);
campaign.initializeCampaign(
creator,
1,
address(this),
"Test Campaign",
"Description",
1000 ether,
address(0),
30 days
);
}
function testDonation() public {
vm.deal(donor, 10 ether);
vm.prank(donor);
campaign.donate{value: 10 ether}(10 ether);
assertEq(campaign.getTotalRaised(), 10 ether);
assertEq(campaign.getDonorBalance(donor), 10 ether);
}
function testRefundOnFailedCampaign() public {
// Donate
vm.deal(donor, 10 ether);
vm.prank(donor);
campaign.donate{value: 10 ether}(10 ether);
// Fast forward past deadline
vm.warp(block.timestamp + 31 days);
// Finalize (should fail - not enough funds)
campaign.finalizeCampaign();
// Claim refund
vm.prank(donor);
campaign.claimRefund();
assertEq(donor.balance, 10 ether);
assertEq(campaign.getDonorBalance(donor), 0);
}
}We welcome contributions from the community! Here's how to get involved:
-
Fork the Repository
git clone https://github.com/YourUsername/ecovault-finance.git cd ecovault-finance -
Create a Feature Branch
git checkout -b feature/awesome-feature
-
Make Your Changes
- Write clean, documented code
- Follow Solidity style guide
- Add/update tests
- Update documentation
-
Test Your Changes
forge test forge fmt -
Commit Your Changes
git commit -m "feat: add awesome feature"Commit Message Format:
feat:New featurefix:Bug fixdocs:Documentation updaterefactor:Code refactoringtest:Test updateschore:Maintenance tasks
-
Push and Create PR
git push origin feature/awesome-feature
Open a Pull Request with:
- Clear description of changes
- Link to related issues
- Test results
- Breaking changes (if any)
Code Style:
- Follow Solidity Style Guide
- Use
forge fmtbefore committing - Keep functions under 50 lines
- Add NatSpec comments
Testing:
- Write tests for new features
- Maintain >80% code coverage
- Include edge cases
- Test gas optimization claims
Security:
- Follow Consensys Best Practices
- Use OpenZeppelin libraries
- Add access control modifiers
- Document all assumptions
Found a bug? Have a feature request?
- Search existing issues to avoid duplicates
- Create a new issue with:
- Clear title
- Detailed description
- Steps to reproduce (for bugs)
- Expected vs actual behavior
- Environment details
- Automated checks must pass (tests, linting)
- At least 2 core team approvals required
- Security review for critical changes
- Documentation must be updated
- Merge after all feedback addressed
This project is licensed under the MIT License.
MIT License
Copyright (c) 2025 EcoVault Finance
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
EcoVault Finance Development Team
- Building the future of decentralized impact funding
- Passionate about blockchain, sustainability, and social impact
- Website: https://ecovault.finance
- Twitter: @EcoVaultFinance
- Discord: Join our community
- GitHub: EcoVault-Finance
- Email: dev@ecovault.finance
Found a security vulnerability? Please DO NOT open a public issue.
Report security issues to: security@ecovault.finance
We take security seriously and will respond within 48 hours.
- Diamond proxy architecture
- Identity management (ONCHAINID)
- Campaign creation & donations
- Milestone-based funding
- Basic DAO voting
- Chainlink Automation integration
- Oracle-based evidence verification
- Advanced voting mechanisms
- Mobile app (React Native)
- Frontend dashboard
- Layer 2 deployment (Optimism/Arbitrum)
- Cross-chain bridge
- ZK rollup voting
- Enhanced analytics
- API for third-party integrations
- Grant programs
- Impact NFTs
- Carbon credit integration
- Partnership integrations
- Developer SDK
Built with support from:
- OpenZeppelin: Security library foundations
- Chainlink: Oracle and automation infrastructure
- ONCHAINID: Decentralized identity standard
- Foundry: Development toolkit
- Ethereum Community: Ongoing support and feedback
Special thanks to all contributors who have helped shape this project.
- EIP-2535 Diamond Standard
- ONCHAINID Documentation
- Chainlink Documentation
- OpenZeppelin Contracts
- Foundry Book
- Solidity Documentation
Built with ❤️ for a sustainable future