Example: Solidity contract


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

contract Ownable {
    address public owner;
    error OwnerRequired();

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, OwnerRequired());
        _;
    }
}

contract Crowdfunding is Ownable {
    error InvalidPayment(uint256 value);
    error DonationRequiredToVote();
    error OnlyOneVoteAllowed();
    error InvalidProjectIndex();
    error InvalidProjectsBatch();
    error VoteAlreadyOver();
    error VoteIsNotOver();
    error OnlyWinningBeneficiaryCanWithdraw();
    error ContractBalanceIsEmpty();
    error TransferFailed();
    
    event NewProjectAdded(uint256 index, string name);
    event NewProjectsBatchAdded(uint256 startIndex, uint256 count);
    event NewDonationReceived(uint256 amount);
    event WinnerChosen(uint256 index, string name);

    modifier onlyDuringStatus(CrowdfundingStatus expectedStatus) {
        if (status != expectedStatus) {
            if (expectedStatus == CrowdfundingStatus.VOTING) {
                revert VoteAlreadyOver();
            } else {
                revert VoteIsNotOver();
            }
        }
        _;
    }

    struct Project {
        string name;
        uint256 votes;
        address beneficiary;
    }

    Project[] public projects;
    mapping(address => uint256) public donations;
    mapping(address => bool) public voted;

    enum CrowdfundingStatus { VOTING, FINISHED }
    CrowdfundingStatus public status;
    uint256 public winningProjectIndex;



    function addProject(string calldata name, address beneficiary) external onlyOwner onlyDuringStatus(CrowdfundingStatus.VOTING) {
        projects.push(Project(name, 0, beneficiary));
        emit NewProjectAdded(projects.length - 1, name);
    }

    function addProjectsBatch(string[] calldata names, address[] calldata beneficiaries) external onlyOwner onlyDuringStatus(CrowdfundingStatus.VOTING) {
        require(names.length == beneficiaries.length, InvalidProjectsBatch());

        uint256 startIndex = projects.length;
        for (uint256 index=0; index<names.length; index++) {
            projects.push(Project(names[index], 0, beneficiaries[index]));
        }

        emit NewProjectsBatchAdded(startIndex, names.length);
    }

    function donate() external payable onlyDuringStatus(CrowdfundingStatus.VOTING) {
        require(msg.value > 0, InvalidPayment(msg.value));
        donations[msg.sender] += msg.value;
        emit NewDonationReceived(msg.value);
    }

    function vote(uint256 projectIndex) external onlyDuringStatus(CrowdfundingStatus.VOTING) {
        require(donations[msg.sender] > 0, DonationRequiredToVote());
        require(!voted[msg.sender], OnlyOneVoteAllowed());
        require(projectIndex < projects.length, InvalidProjectIndex());

        voted[msg.sender] = true;
        projects[projectIndex].votes++;

        if (projects[projectIndex].votes > projects[winningProjectIndex].votes) {
            winningProjectIndex = projectIndex;
        }
    }

    function closeVoting() external onlyOwner onlyDuringStatus(CrowdfundingStatus.VOTING) {
        require(projects.length > 0, InvalidProjectIndex());
        status = CrowdfundingStatus.FINISHED;
        emit WinnerChosen(winningProjectIndex, projects[winningProjectIndex].name);
    }

    function withdraw() external onlyDuringStatus(CrowdfundingStatus.FINISHED) {
        require(projects[winningProjectIndex].beneficiary == msg.sender, OnlyWinningBeneficiaryCanWithdraw());
        require(address(this).balance > 0, ContractBalanceIsEmpty());

        (bool success, ) = payable(msg.sender).call{ value: address(this).balance }("");
        require(success, TransferFailed());
    }

    function getWinningProject() external view onlyDuringStatus(CrowdfundingStatus.FINISHED) returns (uint256 index, string memory name) {
        return (winningProjectIndex, projects[winningProjectIndex].name);
    }
}