What it does: Donations are tracked on-chain with milestone-based releases and public reports showing where every coin goes.
Why it matters: Reduces mistrust in charities.
How it works:
Donations stored in escrow.
Charities upload evidence for milestones.
Independent validators approve releases.
Donors view real-time progress.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;
/**
* @title Charity Impact Transparency Contract
*
* Model:
* - Charity creates a campaign
* - Donors contribute into escrow
* - Funds release only when milestones + impact reports are approved
* - Independent auditor verifies impact reports
* - If campaign fails or expires → donors may refund
*
* NOTES:
* - Uses ETH for simplicity; can be upgraded to ERC20 variants
*/
contract CharityImpactTransparency {
enum CampaignStatus {
Active,
Completed,
Failed,
Withdrawn
}
enum MilestoneStatus {
Pending,
Submitted,
Approved,
Released
}
struct Milestone {
string title;
uint256 unlockAmount;
string impactReport; // IPFS/URL hash describing results
MilestoneStatus status;
}
address public charity;
address public auditor;
string public campaignName;
string public campaignDescription;
string public campaignUrl;
uint256 public goalAmount;
uint256 public deadline;
uint256 public totalRaised;
CampaignStatus public status;
Milestone[] public milestones;
mapping(address => uint256) public contributions;
event Donation(address indexed donor, uint256 amount);
event CampaignFailed();
event CampaignCompleted();
event MilestoneAdded(uint256 index, string title, uint256 unlockAmount);
event MilestoneSubmitted(uint256 index, string report);
event MilestoneApproved(uint256 index);
event MilestoneReleased(uint256 index, uint256 amount);
event Refund(address indexed donor, uint256 amount);
event AuditorUpdated(address indexed auditor);
event FundsWithdrawn(uint256 amount);
modifier onlyCharity() {
require(msg.sender == charity, "Not charity");
_;
}
modifier onlyAuditor() {
require(msg.sender == auditor, "Not auditor");
_;
}
modifier inCampaignStatus(CampaignStatus expected) {
require(status == expected, "Invalid campaign status");
_;
}
constructor(
string memory _name,
string memory _description,
string memory _url,
uint256 _goalAmount,
uint256 _durationSeconds,
address _auditor
) {
require(_goalAmount > 0, "Goal must be > 0");
require(_durationSeconds > 0, "Invalid duration");
require(_auditor != address(0), "Invalid auditor");
charity = msg.sender;
auditor = _auditor;
campaignName = _name;
campaignDescription = _description;
campaignUrl = _url;
goalAmount = _goalAmount;
deadline = block.timestamp + _durationSeconds;
status = CampaignStatus.Active;
}
// --------------------------------------
// DONATIONS
// --------------------------------------
function donate() external payable inCampaignStatus(CampaignStatus.Active) {
require(block.timestamp 0, "No value sent");
contributions[msg.sender] += msg.value;
totalRaised += msg.value;
emit Donation(msg.sender, msg.value);
if (totalRaised >= goalAmount) {
status = CampaignStatus.Completed;
emit CampaignCompleted();
}
}
// --------------------------------------
// ADMINISTRATION
// --------------------------------------
function updateAuditor(address _auditor) external onlyCharity {
require(_auditor != address(0), "Invalid auditor");
auditor = _auditor;
emit AuditorUpdated(_auditor);
}
// --------------------------------------
// MILESTONES (TRANSPARENCY CONTROL)
// --------------------------------------
function addMilestone(
string calldata title,
uint256 unlockAmount
) external onlyCharity inCampaignStatus(CampaignStatus.Active) {
require(unlockAmount > 0, "Invalid amount");
milestones.push(
Milestone({
title: title,
unlockAmount: unlockAmount,
impactReport: "",
status: MilestoneStatus.Pending
})
);
emit MilestoneAdded(milestones.length - 1, title, unlockAmount);
}
function submitImpactReport(
uint256 index,
string calldata reportHash
) external onlyCharity {
require(index < milestones.length, "Invalid milestone");
Milestone storage m = milestones[index];
require(
m.status == MilestoneStatus.Pending ||
m.status == MilestoneStatus.Submitted,
"Invalid milestone state"
);
// Attach verifiable off-chain proof link (IPFS, Arweave, etc.)
m.impactReport = reportHash;
m.status = MilestoneStatus.Submitted;
emit MilestoneSubmitted(index, reportHash);
}
function approveMilestone(uint256 index) external onlyAuditor {
require(index 0, "No report");
m.status = MilestoneStatus.Approved;
emit MilestoneApproved(index);
}
function releaseMilestone(uint256 index)
external
onlyCharity
{
require(index = m.unlockAmount, "Insufficient balance");
m.status = MilestoneStatus.Released;
payable(charity).transfer(m.unlockAmount);
emit MilestoneReleased(index, m.unlockAmount);
}
// --------------------------------------
// CAMPAIGN LIFECYCLE
// --------------------------------------
function finalize() external {
require(
status == CampaignStatus.Active ||
status == CampaignStatus.Completed,
"Already finalized"
);
require(block.timestamp >= deadline, "Too early");
if (totalRaised 0, "Nothing to refund");
contributions[msg.sender] = 0;
payable(msg.sender).transfer(amount);
emit Refund(msg.sender, amount);
}
// --------------------------------------
// EMERGENCY / REMAINDER WITHDRAWAL
// --------------------------------------
function withdrawRemainder()
external
onlyCharity
inCampaignStatus(CampaignStatus.Completed)
{
// Charity may withdraw only after all milestones released or none exist
bool allReleased = true;
for (uint256 i = 0; i < milestones.length; i++) {
if (milestones[i].status != MilestoneStatus.Released) {
allReleased = false;
break;
}
}
require(allReleased, "Pending milestones");
status = CampaignStatus.Withdrawn;
uint256 amount = address(this).balance;
payable(charity).transfer(amount);
emit FundsWithdrawn(amount);
}
// --------------------------------------
// VIEW HELPERS
// --------------------------------------
function milestoneCount() external view returns (uint256) {
return milestones.length;
}
function getMilestone(uint256 index)
external
view
returns (
string memory title,
uint256 unlockAmount,
string memory impactReport,
MilestoneStatus milestoneStatus
)
{
Milestone memory m = milestones[index];
return (m.title, m.unlockAmount, m.impactReport, m.status);
}
}
Build and Grow By Nam Le Thanh