Social Challenge Contract (Fun + Accountability)

What it does: Friends commit to challenges (fitness, reading, learning). Failures result in automatic penalties or donations to charity.

Why it matters: Adds accountability while making habits enjoyable.

How it works:

  • Participants stake funds.

  • Proof of completion submitted via oracles.

  • If someone fails, penalties execute automatically.

  • Leaderboards and badges kept on-chain.

      // SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * SOCIAL CHALLENGE CONTRACT
 * Gamified accountability:
 * Stake → Complete Challenge → Get Stake Back
 * Fail → Stake goes to Charity
 */

contract SocialChallenge {
    enum Status {
        Open,
        Active,
        Completed,
        Settled,
        Cancelled
    }

    enum ProofStatus {
        None,
        Submitted,
        Approved,
        Rejected
    }

    struct Participant {
        uint256 stake;
        bool joined;
        ProofStatus proofStatus;
        string proofNote;
        bool paidOut;
    }

    struct Challenge {
        string title;
        string description;
        address organizer;
        address charity;
        uint256 minStake;
        uint256 startTime;
        uint256 endTime;
        Status status;
        address[] participants;
        mapping(address => Participant) participantData;
        address verifier; // optional third-party verifier
        bool autoApprove; // organizer verifies if false, else verifier/organizer
    }

    uint256 public challengeCount;
    mapping(uint256 => Challenge) private challenges;

    event ChallengeCreated(
        uint256 indexed challengeId,
        address indexed organizer,
        string title,
        uint256 startTime,
        uint256 endTime
    );

    event JoinedChallenge(
        uint256 indexed challengeId,
        address indexed participant,
        uint256 stake
    );

    event ProofSubmitted(
        uint256 indexed challengeId,
        address indexed participant,
        string note
    );

    event ProofReviewed(
        uint256 indexed challengeId,
        address indexed participant,
        bool approved
    );

    event Settled(
        uint256 indexed challengeId
    );

    modifier onlyOrganizer(uint256 challengeId) {
        require(msg.sender == challenges[challengeId].organizer, "Not organizer");
        _;
    }

    modifier onlyVerifierOrOrganizer(uint256 challengeId) {
        Challenge storage c = challenges[challengeId];
        require(
            msg.sender == c.verifier || msg.sender == c.organizer,
            "Not authorized"
        );
        _;
    }

    modifier challengeExists(uint256 challengeId) {
        require(challengeId  startTime, "Invalid times");
        require(minStake > 0, "Min stake must be > 0");

        uint256 id = challengeCount;

        Challenge storage c = challenges[id];
        c.title = title;
        c.description = description;
        c.organizer = msg.sender;
        c.charity = charity;
        c.minStake = minStake;
        c.startTime = startTime;
        c.endTime = endTime;
        c.status = Status.Open;
        c.verifier = verifier;
        c.autoApprove = autoApprove;

        challengeCount++;

        emit ChallengeCreated(id, msg.sender, title, startTime, endTime);
        return id;
    }

    /**
     * Join challenge by staking ETH
     */
    function join(uint256 challengeId)
        external
        payable
        challengeExists(challengeId)
    {
        Challenge storage c = challenges[challengeId];

        require(block.timestamp = c.minStake, "Not enough stake");

        c.participants.push(msg.sender);
        c.participantData[msg.sender] = Participant({
            stake: msg.value,
            joined: true,
            proofStatus: ProofStatus.None,
            proofNote: "",
            paidOut: false
        });

        emit JoinedChallenge(challengeId, msg.sender, msg.value);
    }

    /**
     * Organizer locks the challenge when it starts
     */
    function activate(uint256 challengeId)
        external
        onlyOrganizer(challengeId)
        challengeExists(challengeId)
    {
        Challenge storage c = challenges[challengeId];
        require(block.timestamp >= c.startTime, "Too early");
        require(c.status == Status.Open, "Not open");
        c.status = Status.Active;
    }

    /**
     * Submit proof (simple text or link)
     * In a production system, proof would come via oracles (photos, APIs, etc.)
     */
    function submitProof(uint256 challengeId, string calldata note)
        external
        challengeExists(challengeId)
    {
        Challenge storage c = challenges[challengeId];
        Participant storage p = c.participantData[msg.sender];

        require(c.status == Status.Active, "Challenge not active");
        require(p.joined, "Not participant");
        require(block.timestamp  c.endTime, "Not finished yet");
        require(
            c.status == Status.Active || c.status == Status.Completed,
            "Already settled"
        );

        c.status = Status.Settled;

        uint256 totalToCharity = 0;

        for (uint256 i = 0; i  0) {
            payable(c.charity).transfer(totalToCharity);
        }

        emit Settled(challengeId);
    }

    /**
     * View helpers
     */

    function getChallenge(
        uint256 challengeId
    )
        external
        view
        challengeExists(challengeId)
        returns (
            string memory,
            string memory,
            address,
            address,
            uint256,
            uint256,
            uint256,
            Status,
            address,
            bool,
            uint256
        )
    {
        Challenge storage c = challenges[challengeId];
        return (
            c.title,
            c.description,
            c.organizer,
            c.charity,
            c.minStake,
            c.startTime,
            c.endTime,
            c.status,
            c.verifier,
            c.autoApprove,
            c.participants.length
        );
    }

    function getParticipant(
        uint256 challengeId,
        address user
    )
        external
        view
        challengeExists(challengeId)
        returns (
            uint256,
            bool,
            ProofStatus,
            string memory,
            bool
        )
    {
        Participant storage p = challenges[challengeId].participantData[user];
        return (p.stake, p.joined, p.proofStatus, p.proofNote, p.paidOut);
    }
}