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