From 0d5a2b94ac4d40f0caf1f3389e059d6062ed3f1d Mon Sep 17 00:00:00 2001 From: khayss Date: Sat, 24 Aug 2024 18:25:33 +0100 Subject: [PATCH] feat: design and implement crowdfunding smart contract --- script/Crowdfund.s.sol | 17 +++++ src/Crowdfund.sol | 159 +++++++++++++++++++++++++++++++++++++++++ test/Crowdfund.t.sol | 44 ++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 script/Crowdfund.s.sol create mode 100644 src/Crowdfund.sol create mode 100644 test/Crowdfund.t.sol diff --git a/script/Crowdfund.s.sol b/script/Crowdfund.s.sol new file mode 100644 index 0000000..828570c --- /dev/null +++ b/script/Crowdfund.s.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Crowdfund} from "../src/Crowdfund.sol"; +import {Script, console} from "forge-std/Script.sol"; + +contract CrowdfundDeploy is Script { + function run() external returns (Crowdfund) { + vm.startBroadcast(); + + Crowdfund crowdfund = new Crowdfund(); + + vm.stopBroadcast(); + + return crowdfund; + } +} diff --git a/src/Crowdfund.sol b/src/Crowdfund.sol new file mode 100644 index 0000000..abbc3d8 --- /dev/null +++ b/src/Crowdfund.sol @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +/// @title Crowdfund +/// @notice A crowd funding smart contract that allows users to create and participate in crowdfunding campaigns. +contract Crowdfund { + // Types + + struct Campaign { + bool isInitialized; + bool isPaidOut; + string title; + string description; + address benefactor; + uint goal; + uint deadline; + uint amountRaised; + } + + // State variables + mapping(uint => Campaign) campaigns; + mapping(address => uint[]) userCampaigns; + uint numCampaigns; + uint totalFunding; + + // Events + event CampaignCreated(address indexed creator, uint indexed campaignId); + event CampaignEnded( + address indexed creator, + uint indexed campaignId, + uint indexed amountRaised + ); + event DonationReceived( + uint indexed campaignId, + address indexed donator, + uint indexed amount + ); + + // Errors + error Crowdfund_InvalidCampaign(uint id); + error Crowdfund_CannotDonateZero(); + error Crowdfund_CampaignInactive(); + error Crowdfund_CampaignNotEnded(uint deadline); + error Crowdfund_NoFundsRaised(); + error Crowdfund_PayoutFailed(); + + // Modifiers + + // Functions + + constructor() {} + + /// @notice Creates a new crowdfunding campaign. + /// @param _title The title of the campaign. + /// @param _description The description of the campaign. + /// @param _goal The funding goal of the campaign. + /// @param _deadline The deadline for the campaign. + /// @return campaignId The ID of the created campaign. + function createCampaign( + string calldata _title, + string calldata _description, + uint _goal, + uint _deadline + ) public returns (uint campaignId) { + Campaign memory newCampaign = Campaign({ + isInitialized: true, + isPaidOut: false, + title: _title, + description: _description, + benefactor: msg.sender, + goal: _goal, + deadline: block.timestamp + _deadline, + amountRaised: 0 + }); + + campaignId = numCampaigns; + uint[] storage _userCampaigns = userCampaigns[msg.sender]; + + numCampaigns += 1; + + campaigns[campaignId] = newCampaign; + _userCampaigns.push(campaignId); + + emit CampaignCreated(msg.sender, campaignId); + } + + /// @notice Allows a user to donate to a specific campaign. + /// @param campaignId The ID of the campaign to donate to. + function donateToCampaign(uint campaignId) public payable { + if (msg.value == 0) revert Crowdfund_CannotDonateZero(); + Campaign storage campaign = campaigns[campaignId]; + if (!campaign.isInitialized) + revert Crowdfund_InvalidCampaign(campaignId); + if (block.timestamp > campaign.deadline) + revert Crowdfund_CampaignInactive(); + + totalFunding += msg.value; + campaign.amountRaised += msg.value; + + emit DonationReceived(campaignId, msg.sender, msg.value); + } + + /// @notice Ends a specific campaign and pays out the funds to the benefactor. + /// @param campaignId The ID of the campaign to end. + function endCampaign(uint campaignId) external { + Campaign storage campaign = campaigns[campaignId]; + if (block.timestamp < campaign.deadline) + revert Crowdfund_CampaignNotEnded(campaign.deadline); + + if (campaign.isPaidOut) revert Crowdfund_CampaignInactive(); + + if (campaign.amountRaised == 0) revert Crowdfund_NoFundsRaised(); + + campaign.isPaidOut = true; + totalFunding -= campaign.amountRaised; + + (bool success, ) = payable(campaign.benefactor).call{ + value: campaign.amountRaised + }(""); + + if (!success) revert Crowdfund_PayoutFailed(); + + emit CampaignEnded( + campaign.benefactor, + campaignId, + campaign.amountRaised + ); + } + + /// @notice Gets the total number of campaigns. + /// @return The total number of campaigns. + function getTotalCampaigns() external view returns (uint) { + return numCampaigns; + } + + /// @notice Gets the total funding amount. + /// @return The total funding amount. + function getTotalFunding() external view returns (uint) { + return totalFunding; + } + + /// @notice Gets the campaigns created by a specific user. + /// @param user The address of the user. + /// @return The array of campaign IDs created by the user. + function getUserCampaigns( + address user + ) external view returns (uint[] memory) { + return userCampaigns[user]; + } + + /// @notice Gets the campaign details by ID. + /// @param campaignId The ID of the campaign. + /// @return The campaign details. + function getCampaignById( + uint campaignId + ) external view returns (Campaign memory) { + return campaigns[campaignId]; + } +} diff --git a/test/Crowdfund.t.sol b/test/Crowdfund.t.sol new file mode 100644 index 0000000..6f72c12 --- /dev/null +++ b/test/Crowdfund.t.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {Crowdfund} from "../src/Crowdfund.sol"; +import {Test, console} from "forge-std/Test.sol"; + +contract CrowdfundTest is Test { + Crowdfund public crowdfund; + address fakeUser = vm.addr(1); + + function setUp() public { + crowdfund = new Crowdfund(); + } + + function test_IsInitializedCorrectly() public view { + assertEq(crowdfund.getTotalCampaigns(), 0); + assertEq(crowdfund.getTotalFunding(), 0); + assertEq(crowdfund.getUserCampaigns(fakeUser).length, 0); + } + + function test_CanCreateCampaign() public { + vm.deal(fakeUser, 1 ether); + + string memory title = "Test Campaign"; + string memory description = "This is a test campaign."; + uint goal = 1 ether; + uint deadline = 1 minutes; + + vm.prank(fakeUser); + uint campaignId = crowdfund.createCampaign( + title, + description, + goal, + deadline + ); + + + assertEq(campaignId, 0); + assertEq(crowdfund.getTotalCampaigns(), 1); + assertEq(crowdfund.getTotalFunding(), 0); + // assertEq(crowdfund.getUserCampaigns(fakeUser).length, 1); + // console.log(crowdfund.getUserCampaigns(fakeUser)); + } +}