Smart Contract-Based Crowdfunding Solidity, JavaScript
👤 Sharing: AI
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Crowdfunding {
// Struct to represent a campaign
struct Campaign {
address payable creator; // Address of the campaign creator
string title; // Title of the campaign
string description; // Description of the campaign
uint256 goal; // Funding goal in wei
uint256 deadline; // Deadline of the campaign (Unix timestamp)
uint256 raisedAmount; // Amount raised so far in wei
bool completed; // Whether the goal was reached
bool refunded; // Whether funds have been refunded
}
// Mapping from campaign ID to Campaign struct
mapping(uint256 => Campaign) public campaigns;
// Counter for campaign IDs
uint256 public campaignCount;
// Mapping to track contributions for each campaign and address
mapping(uint256 => mapping(address => uint256)) public contributions;
// Events for logging important actions
event CampaignCreated(uint256 campaignId, address creator, string title, uint256 goal, uint256 deadline);
event ContributionMade(uint256 campaignId, address contributor, uint256 amount);
event CampaignCompleted(uint256 campaignId);
event FundsClaimed(uint256 campaignId, address receiver, uint256 amount);
event FundsRefunded(uint256 campaignId, address contributor, uint256 amount);
// Modifier to check if the campaign deadline has passed
modifier isWithinDeadline(uint256 _campaignId) {
require(block.timestamp < campaigns[_campaignId].deadline, "Campaign deadline has passed.");
_;
}
// Modifier to check if the caller is the campaign creator
modifier onlyCreator(uint256 _campaignId) {
require(msg.sender == campaigns[_campaignId].creator, "Only the campaign creator can call this function.");
_;
}
// Function to create a new crowdfunding campaign
function createCampaign(
string memory _title,
string memory _description,
uint256 _goal,
uint256 _deadline
) public {
require(_goal > 0, "Goal must be greater than 0.");
require(_deadline > block.timestamp, "Deadline must be in the future.");
campaignCount++;
campaigns[campaignCount] = Campaign({
creator: payable(msg.sender),
title: _title,
description: _description,
goal: _goal,
deadline: _deadline,
raisedAmount: 0,
completed: false,
refunded: false
});
emit CampaignCreated(campaignCount, msg.sender, _title, _goal, _deadline);
}
// Function to contribute to a campaign
function contribute(uint256 _campaignId) public payable isWithinDeadline(_campaignId) {
require(campaigns[_campaignId].raisedAmount < campaigns[_campaignId].goal, "Campaign goal already reached.");
require(msg.value > 0, "Contribution amount must be greater than 0.");
campaigns[_campaignId].raisedAmount += msg.value;
contributions[_campaignId][msg.sender] += msg.value;
emit ContributionMade(_campaignId, msg.sender, msg.value);
// Check if the campaign is completed after the contribution
if (campaigns[_campaignId].raisedAmount >= campaigns[_campaignId].goal) {
campaigns[_campaignId].completed = true;
emit CampaignCompleted(_campaignId);
}
}
// Function for the campaign creator to claim the funds if the goal was reached
function claimFunds(uint256 _campaignId) public onlyCreator(_campaignId) {
require(campaigns[_campaignId].completed, "Campaign goal not reached.");
require(!campaigns[_campaignId].refunded, "Campaign already refunded.");
require(address(this).balance >= campaigns[_campaignId].raisedAmount, "Contract balance insufficient.");
uint256 amount = campaigns[_campaignId].raisedAmount;
campaigns[_campaignId].raisedAmount = 0; // Reset raised amount
campaigns[_campaignId].refunded = true; // Mark as refunded (prevents multiple claims)
(bool success, ) = campaigns[_campaignId].creator.call{value: amount}(""); // Transfer funds
require(success, "Transfer failed.");
emit FundsClaimed(_campaignId, campaigns[_campaignId].creator, amount);
}
// Function for contributors to request a refund if the campaign failed
function refund(uint256 _campaignId) public {
require(block.timestamp > campaigns[_campaignId].deadline, "Campaign deadline has not passed yet.");
require(!campaigns[_campaignId].completed, "Campaign was successful and funds have been claimed.");
require(contributions[_campaignId][msg.sender] > 0, "You have not contributed to this campaign.");
uint256 amount = contributions[_campaignId][msg.sender];
contributions[_campaignId][msg.sender] = 0; // Reset contribution for this address
(bool success, ) = payable(msg.sender).call{value: amount}(""); // Transfer funds
require(success, "Transfer failed.");
emit FundsRefunded(_campaignId, msg.sender, amount);
}
// Function to get campaign details
function getCampaign(uint256 _campaignId) public view returns (
address creator,
string memory title,
string memory description,
uint256 goal,
uint256 deadline,
uint256 raisedAmount,
bool completed,
bool refunded
) {
Campaign storage campaign = campaigns[_campaignId];
return (
campaign.creator,
campaign.title,
campaign.description,
campaign.goal,
campaign.deadline,
campaign.raisedAmount,
campaign.completed,
campaign.refunded
);
}
// Fallback function to prevent accidental sending of ether to the contract
receive() external payable {
revert("This contract does not accept direct ether transfers.");
}
}
```
**Explanation:**
* **`pragma solidity ^0.8.0;`**: Specifies the Solidity compiler version.
* **`contract Crowdfunding { ... }`**: Defines the Crowdfunding contract.
* **`struct Campaign { ... }`**: Defines a struct to represent a crowdfunding campaign. It includes:
* `creator`: The address of the campaign creator (a `payable` address to allow for fund transfers).
* `title`: The campaign title.
* `description`: A description of the campaign.
* `goal`: The funding goal (in wei).
* `deadline`: The deadline for the campaign (Unix timestamp).
* `raisedAmount`: The amount of funds raised so far (in wei).
* `completed`: A boolean indicating whether the campaign goal has been reached.
* `refunded`: A boolean indicating whether funds have been refunded to contributors (in case the campaign fails).
* **`mapping(uint256 => Campaign) public campaigns;`**: A mapping from campaign ID (an integer) to the `Campaign` struct. This allows you to retrieve campaign information by its ID.
* **`uint256 public campaignCount;`**: A counter to keep track of the number of campaigns created. It's incremented each time a new campaign is created, acting as a unique ID.
* **`mapping(uint256 => mapping(address => uint256)) public contributions;`**: A nested mapping to track contributions for each campaign and each address. `contributions[campaignId][contributorAddress]` stores the amount (in wei) that a specific address has contributed to a specific campaign.
* **`event CampaignCreated(...);`**, **`event ContributionMade(...);`**, **`event CampaignCompleted(...);`**, **`event FundsClaimed(...);`**, **`event FundsRefunded(...);`**: Events that are emitted when specific actions occur (creating a campaign, making a contribution, completing a campaign, claiming funds, and refunding funds). Events provide a way for external applications (like a frontend) to listen for and react to changes in the contract's state.
* **`modifier isWithinDeadline(uint256 _campaignId) { ... }`**: A modifier to check if the current block timestamp is before the campaign's deadline. This is used to prevent contributions after the deadline.
* **`modifier onlyCreator(uint256 _campaignId) { ... }`**: A modifier to restrict access to certain functions to only the campaign creator.
* **`createCampaign(string memory _title, string memory _description, uint256 _goal, uint256 _deadline) public`**: Creates a new campaign.
* Takes the campaign title, description, goal (in wei), and deadline (Unix timestamp) as input.
* Validates that the goal is greater than 0 and the deadline is in the future.
* Increments the `campaignCount`.
* Creates a new `Campaign` struct and stores it in the `campaigns` mapping.
* Emits a `CampaignCreated` event.
* **`contribute(uint256 _campaignId) public payable isWithinDeadline(_campaignId)`**: Allows users to contribute to a campaign.
* Takes the campaign ID as input.
* Uses the `isWithinDeadline` modifier to ensure the deadline hasn't passed.
* Validates that the campaign's goal hasn't already been reached and that the contribution amount is greater than 0.
* Increases the `raisedAmount` for the campaign.
* Updates the `contributions` mapping to track the contribution from the sender.
* Emits a `ContributionMade` event.
* Checks if the campaign is completed (goal reached) after the contribution. If so, sets `completed` to `true` and emits a `CampaignCompleted` event.
* **`claimFunds(uint256 _campaignId) public onlyCreator(_campaignId)`**: Allows the campaign creator to claim the funds if the campaign was successful.
* Takes the campaign ID as input.
* Uses the `onlyCreator` modifier to ensure only the creator can call this function.
* Validates that the campaign is completed and hasn't already been refunded.
* Transfers the raised amount to the creator's address using `payable(campaigns[_campaignId].creator).transfer(campaigns[_campaignId].raisedAmount)`. **Important:** The older `transfer()` function has a limited gas stipend, which can cause issues with more complex fallback functions on the receiver's side. I've updated it to use `.call{value: amount}("")` which is the recommended way to send ether.
* Emits a `FundsClaimed` event.
* **`refund(uint256 _campaignId) public`**: Allows contributors to request a refund if the campaign failed.
* Takes the campaign ID as input.
* Validates that the deadline has passed, the campaign was not successful, and the sender has contributed to the campaign.
* Transfers the contribution amount back to the sender using `payable(msg.sender).transfer(amount)`. Again, using `.call{value: amount}("")` for gas limit reasons.
* Emits a `FundsRefunded` event.
* **`getCampaign(uint256 _campaignId) public view returns (...)`**: A getter function to retrieve all the details of a campaign, making it easier for external applications to read the campaign data.
* **`receive() external payable { ... }`**: A fallback function that prevents users from accidentally sending ether directly to the contract. It's good practice to include this to avoid unexpected behavior.
**JavaScript (Example for interacting with the contract):**
This JavaScript code assumes you're using a library like Web3.js or Ethers.js to interact with the Ethereum blockchain. You'll need to have a provider (e.g., MetaMask) connected to your browser.
```javascript
// Assuming you have Web3.js or Ethers.js initialized
// Contract ABI (Application Binary Interface - copy this from the Solidity compiler)
const crowdfundingABI = [
{
"inputs": [
{
"internalType": "string",
"name": "_title",
"type": "string"
},
{
"internalType": "string",
"name": "_description",
"type": "string"
},
{
"internalType": "uint256",
"name": "_goal",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_deadline",
"type": "uint256"
}
],
"name": "createCampaign",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "campaignId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"indexed": false,
"internalType": "string",
"name": "title",
"type": "string"
},
{
"indexed": false,
"internalType": "uint256",
"name": "goal",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
}
],
"name": "CampaignCreated",
"type": "event"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_campaignId",
"type": "uint256"
}
],
"name": "contribute",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "uint256",
"name": "campaignId",
"type": "uint256"
},
{
"indexed": false,
"internalType": "address",
"name": "contributor",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "ContributionMade",
"type": "event"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_campaignId",
"type": "uint256"
}
],
"name": "claimFunds",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "campaignCount",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_campaignId",
"type": "uint256"
},
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "contributions",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_campaignId",
"type": "uint256"
}
],
"name": "getCampaign",
"outputs": [
{
"internalType": "address",
"name": "creator",
"type": "address"
},
{
"internalType": "string",
"name": "title",
"type": "string"
},
{
"internalType": "string",
"name": "description",
"type": "string"
},
{
"internalType": "uint256",
"name": "goal",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "raisedAmount",
"type": "uint256"
},
{
"internalType": "bool",
"name": "completed",
"type": "bool"
},
{
"internalType": "bool",
"name": "refunded",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_campaignId",
"type": "uint256"
}
],
"name": "refund",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
];
// Contract address (replace with the actual deployed address)
const crowdfundingAddress = "YOUR_CONTRACT_ADDRESS";
// Create a contract instance
const crowdfundingContract = new web3.eth.Contract(crowdfundingABI, crowdfundingAddress);
// Example function to create a campaign
async function createNewCampaign(title, description, goal, deadline) {
try {
// Convert goal to Wei (smallest unit of Ether)
const goalInWei = web3.utils.toWei(goal, 'ether');
// Convert deadline to Unix timestamp (seconds since epoch)
const deadlineTimestamp = Math.floor(new Date(deadline).getTime() / 1000);
// Get accounts from the connected Ethereum provider (e.g., MetaMask)
const accounts = await web3.eth.getAccounts();
const creatorAddress = accounts[0]; // Use the first account as the creator
// Call the createCampaign function on the contract
await crowdfundingContract.methods.createCampaign(title, description, goalInWei, deadlineTimestamp)
.send({ from: creatorAddress }); // Specify the sender
console.log("Campaign created successfully!");
} catch (error) {
console.error("Error creating campaign:", error);
}
}
// Example function to contribute to a campaign
async function contributeToCampaign(campaignId, amountInEther) {
try {
// Convert amount to Wei
const amountInWei = web3.utils.toWei(amountInEther, 'ether');
// Get accounts
const accounts = await web3.eth.getAccounts();
const contributorAddress = accounts[0];
// Send the transaction to contribute
await crowdfundingContract.methods.contribute(campaignId)
.send({ from: contributorAddress, value: amountInWei }); // Specify sender and contribution amount
console.log("Contribution successful!");
} catch (error) {
console.error("Error contributing:", error);
}
}
// Example function to get campaign details
async function getCampaignDetails(campaignId) {
try {
const campaign = await crowdfundingContract.methods.getCampaign(campaignId).call();
console.log("Campaign Details:", campaign);
return campaign;
} catch (error) {
console.error("Error getting campaign details:", error);
return null;
}
}
// Example function to claim funds (only callable by the campaign creator)
async function claimCampaignFunds(campaignId) {
try {
const accounts = await web3.eth.getAccounts();
const creatorAddress = accounts[0]; // Assuming the caller is the creator
await crowdfundingContract.methods.claimFunds(campaignId).send({from: creatorAddress});
console.log("Funds claimed successfully!");
} catch(error) {
console.error("Error claiming funds:", error);
}
}
// Example function to request a refund
async function requestRefund(campaignId) {
try {
const accounts = await web3.eth.getAccounts();
const refundRequester = accounts[0];
await crowdfundingContract.methods.refund(campaignId).send({from: refundRequester});
console.log("Refund requested successfully!");
} catch (error) {
console.error("Error requesting refund:", error);
}
}
// Example usage:
// 1. Create a campaign:
// createNewCampaign("My Awesome Project", "Funding for a new project", "10", "2024-12-31");
// 2. Contribute to a campaign (campaign ID = 1, contribute 1 Ether):
// contributeToCampaign(1, "1");
// 3. Get campaign details (campaign ID = 1):
// getCampaignDetails(1);
// 4. Claim Funds (campaign ID = 1 - only callable by creator after goal is reached)
// claimCampaignFunds(1);
// 5. Request Refund (campaign ID = 1 - only callable if deadline passed and goal not reached)
// requestRefund(1);
```
**JavaScript Explanation:**
1. **ABI (Application Binary Interface):**
* The `crowdfundingABI` is a JSON array that describes the structure of the smart contract. It tells the JavaScript code what functions are available in the contract, what parameters they take, and what they return. You get this ABI from the Solidity compiler after compiling your contract. **Crucially, make sure this matches the compiled contract.**
2. **Contract Address:**
* `crowdfundingAddress` is the address where your smart contract is deployed on the blockchain. You get this address after deploying the contract. **Replace `"YOUR_CONTRACT_ADDRESS"` with the actual address of your deployed contract!**
3. **Contract Instance:**
* `const crowdfundingContract = new web3.eth.Contract(crowdfundingABI, crowdfundingAddress);` creates an instance of the contract in JavaScript, allowing you to interact with it. It uses the ABI and contract address.
4. **`createNewCampaign()` Function:**
* Takes the campaign details (title, description, goal, deadline) as input.
* Converts the goal from Ether to Wei (the smallest unit of Ether) using `web3.utils.toWei()`. The contract stores values in Wei.
* Converts the deadline from a human-readable date string to a Unix timestamp (seconds since the epoch) using `Math.floor(new Date(deadline).getTime() / 1000)`. Solidity stores dates and times as Unix timestamps.
* Gets the available Ethereum accounts using `web3.eth.getAccounts()`. This requires a connected provider like MetaMask.
* Calls the `createCampaign` function of the smart contract using `crowdfundingContract.methods.createCampaign(...)`.
* Uses `send({ from: creatorAddress })` to send the transaction to the blockchain, specifying the address that is calling the function (in this case, the campaign creator). This will prompt the user to approve the transaction in their wallet (e.g., MetaMask).
* Handles potential errors using a `try...catch` block.
5. **`contributeToCampaign()` Function:**
* Takes the campaign ID and the contribution amount (in Ether) as input.
* Converts the amount to Wei.
* Gets the Ethereum accounts.
* Calls the `contribute` function of the smart contract, sending the amount in Wei along with the transaction using `send({ from: contributorAddress, value: amountInWei })`.
6. **`getCampaignDetails()` Function:**
* Takes the campaign ID as input.
* Calls the `getCampaign` function of the smart contract using `crowdfundingContract.methods.getCampaign(campaignId).call()`. The `.call()` method is used for read-only functions that don't modify the blockchain state.
* Logs the campaign details to the console.
7. **`claimCampaignFunds()` Function:**
* Gets the accounts.
* Calls the `claimFunds()` function of the smart contract.
* Sends the transaction using `send({from: creatorAddress})`, ensuring that only the creator can call this function (due to the `onlyCreator` modifier in the contract).
8. **`requestRefund()` Function:**
* Gets the accounts.
* Calls the `refund()` function of the smart contract.
* Sends the transaction using `send({from: refundRequester})`.
9. **Example Usage:**
* Shows how to call the different functions. **Remember to replace placeholder values with your actual values!**
* The commented-out code provides examples of how to use the functions. Uncomment and modify the code as needed to test the contract.
**Important Considerations:**
* **Security:** This is a simplified example and should not be used in a production environment without thorough security auditing. Potential vulnerabilities include:
* **Reentrancy:** A malicious contract could potentially call back into the `contribute` or `refund` functions during the transfer, potentially draining funds. Consider using the "Checks-Effects-Interactions" pattern and/or reentrancy guard libraries like OpenZeppelin's `ReentrancyGuard`.
* **Integer Overflow/Underflow:** While Solidity 0.8.0 and later have built-in overflow/underflow protection, older versions are vulnerable.
* **Denial-of-Service (DoS):** A malicious actor could potentially DoS the contract by making many small contributions, making it difficult for others to contribute or claim refunds.
* **Gas Optimization:** Solidity contracts can be expensive to deploy and execute due to gas costs. Optimize the contract code to reduce gas consumption. This can involve using more efficient data structures, reducing storage operations, and avoiding unnecessary computations.
* **User Experience (UX):** The JavaScript code is a basic example. A real-world application would need a user-friendly interface to interact with the contract, handle errors gracefully, and provide clear feedback to the user.
* **Testing:** Thoroughly test the smart contract using unit tests and integration tests to ensure it functions as expected and is resistant to attacks. Libraries like Truffle and Hardhat provide excellent testing frameworks.
* **Frontend Framework:** You'll typically use a JavaScript frontend framework (like React, Vue.js, or Angular) to build the user interface for your crowdfunding application.
* **Deployment:** You'll need to deploy the contract to a test network (like Ropsten, Rinkeby, Goerli, Sepolia) or the main Ethereum network. This requires setting up a deployment environment (e.g., using Truffle or Hardhat).
This comprehensive example provides a starting point for building a smart contract-based crowdfunding application. Remember to adapt and improve it based on your specific requirements and best practices. Good luck!
👁️ Viewed: 9
Comments