Skip to content

Task 3 #58

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: task-3
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions contracts/CrowdFunding/CrowdFunding.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "./CrowdToken.sol";
import "./CrowdNFT.sol";

contract Crowdfunding {
address public owner;
uint256 public fundingGoal;
uint256 public totalFunds;
uint256 public nftThreshold;
uint256 public tokenConversionRate; // Number of ERC20 tokens minted per wei contributed

CrowdToken public rewardToken;
CrowdNFT public rewardNFT;

mapping(address => uint256) public contributions;

constructor(
uint256 _fundingGoal,
uint256 _nftThreshold,
uint256 _tokenConversionRate,
address _rewardToken,
address _rewardNFT
) {
owner = msg.sender;
fundingGoal = _fundingGoal;
nftThreshold = _nftThreshold;
tokenConversionRate = _tokenConversionRate;
rewardToken = CrowdToken(_rewardToken);
rewardNFT = CrowdNFT(_rewardNFT);
}

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

// Contribute funds and receive rewards.
function contribute() public payable {
require(msg.value > 0, "Send ETH to contribute");
contributions[msg.sender] += msg.value;
totalFunds += msg.value;

// Reward with ERC20 tokens.
uint256 tokenAmount = msg.value * tokenConversionRate;
rewardToken.mint(msg.sender, tokenAmount);

// If contribution meets or exceeds the NFT threshold, mint an NFT.
if (msg.value >= nftThreshold) {
rewardNFT.mintNFT(msg.sender);
}
}

// Owner can withdraw funds if the funding goal is met.
function withdrawFunds() public onlyOwner {
require(totalFunds >= fundingGoal, "Funding goal not met");
uint256 amount = address(this).balance;
totalFunds = 0; // Resetting total funds (note: individual contributions remain recorded)
payable(owner).transfer(amount);
}

// Fallback to allow direct ETH transfers.
receive() external payable {
contribute();
}

/*
Example Usage:
- A contributor sends 1 ETH via contribute(). If nftThreshold is set to 0.5 ETH,
they receive both ERC20 tokens (1 ETH * tokenConversionRate) and an NFT.
- Once total funds reach or exceed fundingGoal, the owner can call withdrawFunds().
*/
}
28 changes: 28 additions & 0 deletions contracts/CrowdFunding/CrowdNFT.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract CrowdNFT is ERC721, Ownable {
uint256 public tokenCounter;
address public minter;

constructor() ERC721("CrowdNFT", "CNFT") Ownable(msg.sender) {
tokenCounter = 0;
}

// Set the authorized minter (typically the crowdfunding contract).
function setMinter(address _minter) external onlyOwner {
minter = _minter;
}

// Mint a new NFT to the specified address; only the authorized minter can call.
function mintNFT(address to) external returns (uint256) {
require(msg.sender == minter, "Not authorized");
uint256 newTokenId = tokenCounter;
_safeMint(to, newTokenId);
tokenCounter++;
return newTokenId;
}
}
23 changes: 23 additions & 0 deletions contracts/CrowdFunding/CrowdToken.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract CrowdToken is ERC20, Ownable {
address public minter;

constructor() ERC20("CrowdToken", "CTK") Ownable(msg.sender) {
// Initialization if needed
}

function setMinter(address _minter) external onlyOwner {
minter = _minter;
}

// Mint tokens; only the authorized minter can call.
function mint(address to, uint256 amount) external {
require(msg.sender == minter, "Not authorized");
_mint(to, amount);
}
}
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"hardhat": "^2.22.18"
},
"dependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.7"
"@nomicfoundation/hardhat-chai-matchers": "^2.0.7",
"@openzeppelin/contracts": "^5.2.0"
},
"scripts": {
"format": "prettier --write \"test/**/*.js\"",
Expand Down
2 changes: 2 additions & 0 deletions submissions/assignment-3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Assignment 3
Here's a link to [assignment 3](../contracts/crowdFunding)
95 changes: 95 additions & 0 deletions test/CrowdFunding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Crowdfunding", function () {
let owner, user1, user2;
let crowdfunding, crowdToken, crowdNFT;
const fundingGoal = ethers.parseEther("5"); // 5 ETH goal
const nftThreshold = ethers.parseEther("1"); // 1 ETH for NFT reward
const tokenConversionRate = 1000; // 1000 CTK per ETH

beforeEach(async function () {
[owner, user1, user2] = await ethers.getSigners();

// Deploy ERC20 token
const CrowdToken = await ethers.getContractFactory("CrowdToken");
crowdToken = await CrowdToken.deploy();
await crowdToken.waitForDeployment();

// Deploy NFT contract
const CrowdNFT = await ethers.getContractFactory("CrowdNFT");
crowdNFT = await CrowdNFT.deploy();
await crowdNFT.waitForDeployment();

// Deploy Crowdfunding contract
const Crowdfunding = await ethers.getContractFactory("Crowdfunding");
crowdfunding = await Crowdfunding.deploy(
fundingGoal,
nftThreshold,
tokenConversionRate,
await crowdToken.getAddress(),
await crowdNFT.getAddress()
);
await crowdfunding.waitForDeployment();

// Set Crowdfunding contract as minter
await crowdToken.setMinter(await crowdfunding.getAddress());
await crowdNFT.setMinter(await crowdfunding.getAddress());
});

it("should deploy with correct initial values", async function () {
expect(await crowdfunding.fundingGoal()).to.equal(fundingGoal);
expect(await crowdfunding.nftThreshold()).to.equal(nftThreshold);
expect(await crowdfunding.tokenConversionRate()).to.equal(tokenConversionRate);
expect(await crowdfunding.owner()).to.equal(owner.address);
});

it("should allow users to contribute and receive ERC20 rewards", async function () {
const contribution = ethers.parseEther("2"); // 2 ETH

await crowdfunding.connect(user1).contribute({ value: contribution });

expect(await crowdfunding.contributions(user1.address)).to.equal(contribution);
expect(await crowdToken.balanceOf(user1.address)).to.equal(contribution * BigInt(tokenConversionRate));
});

it("should mint an NFT if contribution meets the threshold", async function () {
const contribution = ethers.parseEther("1.5"); // 1.5 ETH (above threshold)

await crowdfunding.connect(user1).contribute({ value: contribution });

expect(await crowdNFT.balanceOf(user1.address)).to.equal(1);
});

it("should not mint an NFT if contribution is below threshold", async function () {
const contribution = ethers.parseEther("0.5"); // Below NFT threshold

await crowdfunding.connect(user1).contribute({ value: contribution });

expect(await crowdNFT.balanceOf(user1.address)).to.equal(0);
});

it("should allow the owner to withdraw funds when goal is met", async function () {
const contribution = ethers.parseEther("5"); // Fundraising goal met

await crowdfunding.connect(user1).contribute({ value: contribution });

const ownerBalanceBefore = await ethers.provider.getBalance(owner.address);
await crowdfunding.connect(owner).withdrawFunds();
const ownerBalanceAfter = await ethers.provider.getBalance(owner.address);

expect(ownerBalanceAfter).to.be.greaterThan(ownerBalanceBefore);
});

it("should not allow withdrawal if funding goal is not met", async function () {
const contribution = ethers.parseEther("2"); // Below goal

await crowdfunding.connect(user1).contribute({ value: contribution });

await expect(crowdfunding.connect(owner).withdrawFunds()).to.be.revertedWith("Funding goal not met");
});

it("should reject contributions of 0 ETH", async function () {
await expect(crowdfunding.connect(user1).contribute({ value: 0 })).to.be.revertedWith("Send ETH to contribute");
});
});