diff --git a/crowdfunding/.pretierrc b/crowdfunding/.prettierrc similarity index 71% rename from crowdfunding/.pretierrc rename to crowdfunding/.prettierrc index 627d3e4..b5e3057 100644 --- a/crowdfunding/.pretierrc +++ b/crowdfunding/.prettierrc @@ -4,11 +4,12 @@ "files": "*.sol", "options": { "printWidth": 120, - "tabWidth": 4, + "tabWidth": 2, "useTabs": false, "singleQuote": false, "bracketSpacing": false, - "explicitTypes": "always" + "explicitTypes": "always", + "semi": true } } ] diff --git a/crowdfunding/contracts/CrowdFunding.sol b/crowdfunding/contracts/CrowdFunding.sol index f3d7f56..e971e7e 100644 --- a/crowdfunding/contracts/CrowdFunding.sol +++ b/crowdfunding/contracts/CrowdFunding.sol @@ -5,105 +5,124 @@ pragma solidity ^0.8.7; import "@openzeppelin/contracts/access/Ownable.sol"; contract CrowdFunding is Ownable { - mapping(address => uint256) public contributors; - uint256 public contributorsCount; - uint256 public minContribution; - uint256 public deadline; //timestamp - uint256 public goal; - uint256 public raisedAmount; - - mapping(uint256 => Request) public requests; - uint256 public requestsCount; - - struct Request { - string description; - address recipient; - uint256 value; - bool executed; - uint256 votersCount; - mapping(address => bool) voters; + mapping(address => uint256) public contributors; + uint256 public contributorsCount; + uint256 public minContribution; + uint256 public deadline; //timestamp + uint256 public goal; + uint256 public raisedAmount; + + mapping(uint256 => Request) public requests; + uint256 public requestsCount; + + struct Request { + string description; + address recipient; + uint256 value; + bool executed; + uint256 votersCount; + mapping(address => bool) voters; + } + + event RequestCreated(uint indexed requestId, string description, address indexed recipient, uint value); + + modifier onFailed() { + require(block.timestamp > deadline && raisedAmount < goal); + _; + } + + modifier onSuccess() { + require(raisedAmount >= goal, "the campaign didnt reach its goal yet"); + _; + } + + modifier onlyContributors() { + require(_contributed() > 0, "only contributors can vote"); + _; + } + + modifier notExecuted(uint256 requestId) { + require(requests[requestId].executed == false, 'the request has already been executed'); + _; + } + + constructor(uint256 _goal, uint256 secondsToDeadline) { + goal = _goal; + deadline = block.timestamp + secondsToDeadline; + minContribution = 100 wei; + } + + function contribute() public payable { + require(block.timestamp <= deadline, "Crowdfunding already finished"); + require(msg.value >= minContribution, "minimum contribution not met"); + + if (_contributed() == 0) { + // first time contributor + contributorsCount++; } - modifier onFailed() { - require(block.timestamp > deadline && raisedAmount < goal); - _; - } - - modifier onSuccess() { - require(raisedAmount >= goal, "the campaign didnt reach its goal yet"); - _; - } - - modifier onlyContributors() { - require(_contributed() > 0); - _; - } - - constructor(uint256 _goal, uint256 secondsToDeadline) { - goal = _goal; - deadline = block.timestamp + secondsToDeadline; - minContribution = 100 wei; - } - - function contribute() public payable { - require(block.timestamp <= deadline, "Crowdfunding already finished"); - require(msg.value >= minContribution, "minimum contribution not met"); - - if (_contributed() == 0) { - // first time contributor - contributorsCount++; - } - - _addContribution(msg.value); - raisedAmount += msg.value; - } - - function getRefund() public onlyContributors onFailed { - uint256 refundAmount = _contributed(); - _setContribution(0); - raisedAmount -= refundAmount; - contributorsCount--; - payable(msg.sender).transfer(refundAmount); - } - - receive() external payable { - contribute(); - } - - function _contributed() internal view returns (uint256) { - return contributors[msg.sender]; - } - - function _addContribution(uint256 value) internal { - contributors[msg.sender] += value; - } - - function _setContribution(uint256 value) internal { - contributors[msg.sender] = value; - } - - function createRequest( - string memory description, - address recipient, - uint256 value - ) public onlyOwner { - Request storage request = requests[requestsCount]; - - request.description = description; - request.recipient = recipient; - request.value = value; - - requestsCount++; - } - - function vote(uint256 requestId) public onlyContributors onSuccess { - require( - requests[requestId].executed == false, - "the request has already been executed" - ); - require( - requests[requestId].voters[msg.sender] == false, - "you already voted" - ); - } + _addContribution(msg.value); + raisedAmount += msg.value; + } + + function getRefund() public onlyContributors onFailed { + uint256 refundAmount = _contributed(); + _setContribution(0); + raisedAmount -= refundAmount; + contributorsCount--; + payable(msg.sender).transfer(refundAmount); + } + + receive() external payable { + contribute(); + } + + function _contributed() internal view returns (uint256) { + return contributors[msg.sender]; + } + + function _addContribution(uint256 value) internal { + contributors[msg.sender] += value; + } + + function _setContribution(uint256 value) internal { + contributors[msg.sender] = value; + } + + function createRequest( + string memory description, + address recipient, + uint256 value + ) public onlyOwner returns(uint) { + uint requestId = requestsCount; + Request storage request = requests[requestId]; + + request.description = description; + request.recipient = recipient; + request.value = value; + + requestsCount++; + + emit RequestCreated(requestId, description, recipient, value); + return requestId; + } + + function vote(uint256 requestId) public onlyContributors onSuccess notExecuted(requestId) { + require(requests[requestId].voters[msg.sender] == false, "you already voted"); + + requests[requestId].voters[msg.sender] = true; + requests[requestId].votersCount++; + } + + function didVote(uint256 requestId, address contributor) public view returns (bool) { + return requests[requestId].voters[contributor]; + } + + function makePayment(uint256 requestId) public onlyOwner onSuccess notExecuted(requestId) { + Request storage request = requests[requestId]; + require(request.votersCount >= contributorsCount/2, 'need more than half of contributors votes'); + require(address(this).balance >= request.value, 'not enough funds'); + + payable(request.recipient).transfer(request.value); + } } diff --git a/crowdfunding/test/crowdfunding_test.ts b/crowdfunding/test/crowdfunding_test.ts index 29f291b..b0827eb 100644 --- a/crowdfunding/test/crowdfunding_test.ts +++ b/crowdfunding/test/crowdfunding_test.ts @@ -3,21 +3,24 @@ import { TransactionRequest, TransactionResponse } from '@ethersproject/abstract import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; import { BigNumber, Contract } from 'ethers'; import hre, {ethers} from 'hardhat' +import { MockContract, smock } from '@defi-wonderland/smock' describe("CrowdFunding", function () { - let contract: Contract; + let contract: MockContract; let signer: SignerWithAddress; let otherSigner: SignerWithAddress; + let yetAnotherSigner: SignerWithAddress; beforeEach(async () => { // Deploy contact - const factory = await ethers.getContractFactory("CrowdFunding"); + const factory = await smock.mock("CrowdFunding"); contract = await factory.deploy(ethers.utils.parseEther('10'), 600); await contract.deployed(); // Store signer signer = (await ethers.getSigners())[0] otherSigner = (await ethers.getSigners())[1] + yetAnotherSigner = (await ethers.getSigners())[2] }); describe("contribute", function () { @@ -108,4 +111,85 @@ describe("CrowdFunding", function () { expect(contract.connect(otherSigner).createRequest('Test request', signer.address, 1000)).to.revertedWith('owner') }); }); + + describe("vote", function () { + beforeEach(async () => { + await contract.createRequest('Test request', signer.address, 1000); + }); + + describe("when the campaign was funded", function () { + beforeEach(async () => { + await contract.contribute({value: ethers.utils.parseEther('20')}) + }); + + it("increases vote count and marks the contributor as voted", async () => { + let request = await contract.requests(0) + expect(request.votersCount).to.eq(0) + expect(await contract.didVote(0, signer.address)).to.eq(false) + + await contract.vote(0); + + request = await contract.requests(0) + expect(request.votersCount).to.eq(1) + expect(await contract.didVote(0, signer.address)).to.eq(true) + }); + + it("only allows contributors to vote", async () => { + expect(contract.connect(otherSigner).vote(0)).to.revertedWith('only contributors') + }); + + it("doesnt allow you to vote twice", async () => { + expect(contract.vote(0)).to.not.be.reverted; + expect(contract.vote(0)).to.be.revertedWith('already voted') + }); + + it("doenst allow you to vote if request was already executed", async () => { + await contract.setVariable('requests', { + 0: { + executed: true + } + }) + + expect(contract.vote(0)).to.revertedWith('already been executed') + }); + }); + + describe("when the campaign was not funded", function () { + it("doesnt allow you to vote", async () => { + await contract.contribute({value: 1000}); // user is contributor + expect(contract.vote(0)).to.be.revertedWith('didnt reach its goal') + }); + }); + + }); + + describe("makePayment", function () { + it("transfers eth to the request recipient", async () => { + // contribute until goal is reached + await contract.connect(signer).contribute({value: ethers.utils.parseEther('5')}) + await contract.connect(otherSigner).contribute({value: ethers.utils.parseEther('5')}) + await contract.connect(yetAnotherSigner).contribute({value: ethers.utils.parseEther('5')}) + + const recipient = (await ethers.getSigners())[3] + const recipientOldBalance = await ethers.provider.getBalance(recipient.address) + + const requestValue = ethers.utils.parseEther('1.5'); + const tx = await contract.createRequest('Test request', recipient.address, requestValue) as TransactionResponse + const receipt = await tx.wait() + + const requestId = BigNumber.from(receipt.logs[0].topics[1]) // second value of first emmited event + + await contract.connect(signer).vote(requestId) + await contract.connect(otherSigner).vote(requestId) + + await contract.makePayment(requestId) + + const recipientNewBalance = await ethers.provider.getBalance(recipient.address) + expect(recipientNewBalance.sub(recipientOldBalance)).to.eq(requestValue) + }); + + it("is only callable by owner", async () => { + + }); + }); }); \ No newline at end of file