Charity Impact Transparency Contract

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);
    }
}