Creating a Decentralised Crowd Funding Contract with Solidity

Creating a Decentralised Crowd Funding Contract with Solidity

Osikhena Oshomah's photo
Osikhena Oshomah
·Sep 21, 2022·

21 min read

Featured on Hashnode
Play this article

Table of contents

This will be a three-part series with code and instructions for creating and deploying a decentralized crowdfunding contract with Solidity. You can use a crowdfunding contract to raise money for a specific purpose. A contract can be written in which a person requests donations for a particular purpose. Supporters of their ideas can contribute to the campaign in Ethers. A campaign creator creates milestones that donors must approve before they can withdraw the Ethers donated to them.

Introduction

In this series, we will use Solidity to create a smart contract. We'll make a factory contract that creates child contracts from an implementation contract. This pattern is cost-effective since we are not deploying new child contracts to the network but instead creating clones to which the factory contract will delegate calls. Each child contract or instance of the implementation contract is autonomous, with its own state variables.

We will build a frontend DApp that will connect to the smart contract and allow people to create campaigns to request funds. We will store some information off-chain on the InterPlanetary File System (IPFS) network. We will store the resulting IPFS content identifier on-chain. We will save money on gas by doing it this way instead of storing large amounts of data on-chain.

Let's get right into it and create our project. You can find the code for this tutorial on GitHub

Creating a New Hardhat Project

Steps to create a new hardhat project:

Open our terminal, navigate to the project directory, and then type the command below to initiate a new Node.js project.

  npm init --y

Install hardhat in the project by typing:

  npm install --save-dev hardhat

Run the following command to create a hardhat project and select create a Javascript project

    npm hardhat

The above command will install all dependencies and bootstrap a hardhat project in the directory.

Screenshot from 2022-09-07 15-32-40.png

Configuring the project

Install the dotenv package from NPM to enable us put our secret configuration, such as private keys, inside an environment file.

 npm install dotenv --save

Create a file called .env on the project's root directory. This file will contain the following keys:

INFURA_URL = 
PRIVATE_KEY=
POLY_SCAN=

The INFURA_URL will be our Infura key to connect via Infura to Polygon Mumbai testnet network. You can register for an Infura account here and create a project that points to the Polygon Mumbai network here

The PRIVATE_KEY will be the private key to our wallet that we will use to deploy the smart contract to the blockchain network.

The POLY_SCAN key is our API key that Etherscan will use to verify the contract after deployment. You can signup here to obtain your key.

Note: You should never push the .env file to version control. It would be best if you remembered to add it to your .gitignore file.

Open the hardhat.config.js file in the project's root directory. This is a configuration file used by hardhat. Replace the content with the code below:

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  solidity: {
    compilers: [
      {
        version: "0.8.0",
      },
      {
        version: "0.8.2",
      },
      {
        version: "0.8.1",
      },
    ],
  },
  networks: {
    hardhat: {
      chainId: 1337,
    },
    mumbai: {
      url: process.env.INFURA_URL,
      accounts: [`0x${process.env.PRIVATE_KEY}`],
      chainId: 80001,
      gas: 2100000,
      gasPrice: 8000000000,
    },
  },
  etherscan: {
    apiKey: process.env.POLY_SCAN,
  },
};

At the top of the file, we required the @nomicfoundation/hardhat-toolbox, provided by hardhat. Next, we require("dotenv").config(), this will load our environment variables into the application, and we can access the environment variables by writing:

 processs.env.[Environment_Key]

Following that, we set up the Solidity compiler version that we will use to compile the project. The networks key is configured and inside the networks key, we have a configuration for our local hardhat environment and the mumbai Polygon network. We use the etherscan apiKey to connect to Etherscan for contract verification.

Let's open the package.json file and write some scripts to run the project. Replace the scripts object with the following:

 "scripts": {
    "deploy-local": "npx hardhat run scripts/deploy.js --network hardhat",
    "deploy": "npx hardhat run scripts/deploy.js --network mumbai",
    "run-local": "npx hardhat run scripts/run.js --network hardhat",
    "test": "npx hardhat test --network hardhat ./test/test.js"
  }

These commands will be executed and used to deploy and test our application.

Installing Openzeppelin Contracts

Openzeppelin provides audited and tested smart contracts that you can use in your project. We will add Openzeppelin contracts into our project by typing:

npm install @openzeppelin/contracts

We will use Openzeppelin's Clones, Ownable, and Initializable contracts in our crowdfunding contract project. The Clones contract is used to replicate an implementation contract that has been deployed to the blockchain. The Clones contract duplicates the crowdfunding contract, giving each funding project its own smart contract.

We use the Ownable contract to grant an address ownership of a smart contract, whereas the Initializable contract acts like a constructor by ensuring that a function commonly known as initialize is executed once.

Implementing the Factory Contract Using Openzeppelin Clonable

We will create two files; the contract factory and the crowdfunding contract. Open the contracts folder of the project and create two files, namely CrowdFundingContract.sol and CrowdSourcingFactory.sol. Delete the default contract in the contracts folder. Open the CrowdSourcingFactory.sol file, and start creating the methods and variables in the factory contract.

//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/Clones.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "./CrowdFundingContract.sol";

contract CrowdSourcingFactory is Ownable {
    //state variables;
    address immutable crowdFundingImplementation;
    address[] public _deployedContracts;
    uint256 public fundingFee = 0.001 ether;

    //events
    event newCrowdFundingCreated(
        address indexed owner,
        uint256 amount,
        address cloneAddress,
        string fundingCID
    );

    constructor(address _implementation) Ownable() {
        crowdFundingImplementation = _implementation;
    }

    function createCrowdFundingContract(
        string memory _fundingCId,
        uint256 _amount,
        uint256 _duration
    ) external payable returns (address) {
        require(msg.value >= fundingFee, "deposit too small");
        address clone = Clones.clone(crowdFundingImplementation);
        (bool success, ) = clone.call(
            abi.encodeWithSignature(
                "initialize(string,uint256,uint256)",
                _fundingCId,
                _amount,
                _duration
            )
        );
        require(success, "creation failed");

        _deployedContracts.push(clone);
        emit newCrowdFundingCreated(msg.sender, fundingFee, clone, _fundingCId);
        return clone;
    }

    function withdrawFunds() public onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "nothing to withdraw");
        (bool success, ) = payable(msg.sender).call{value: balance}("");
        require(success, "withdrawal failed");
    }

    function deployedCo`ntracts() public view returns (address[] memory) {
        return _deployedContracts;
    }

    receive() external payable {}
}

We imported the Clones and Ownable contracts from Openzeppelin. The implementation contract CrowdFundingContract.sol is also imported. Our contract factory inherits from the Ownable contract; this means the Ownable contract will manage the ownership of this contract.

contract CrowdSourcingFactory is Ownable

We define three state variables of crowdFundingImplementation, _deployedContracts, and fundingFee. The crowdFundingImplementaion variable is of type address immutable, and we use it to save the address of the deployed CrowdFundingcontract. The _deployedContracts is an array of type addresses used to save the address of each deployed cloned contract. At the same time, the fundingFee is the fee paid before creating and deploying a new CrowdFundingContract`.

The factory contract constructor is called with the deployed CrowdFundingContract address. We save this address in the crowdFundingImplementation variable. The deployer of the factory contract is the owner of the contract. You will notice that we also call the Ownable contract constructor to assign ownership to the deployer of the contract.

Cloning a Contract

Anyone who wants to create a crowdfunding project calls the function createCrowdFundingContract. It accepts a string variable called _fundingCId as a parameter. This is the IPFS hash containing the details of the crowd-sourcing project, which will be too expensive to store on-chain. The other parameters are the _amount you want to raise and the _duration of the campaign. These parameters are both of the 'uint256' type.

The createCrowdFundingContract function is a payable function, meaning the caller of the function must send Ether that is greater or equal to the _funndingFee.

This line clones the CrowdFundingContract:

address clone = Clones.clone(crowdFundingImplementation);

We call the Clones contract from the Openzeppelin clone function, which creates a new clone of the CrowdSourcingContract. The function returns the address of the cloned contract. Each cloned contract has a different independent state. After a successful clone of the implementation contract (CrowdSourcingContract), we call the initialize function using the low-level call method.

The 'initialize' function is similar to a constructor in that we can only call it once.

  (bool success, ) = clone.call(
            abi.encodeWithSignature(
                "initialize(string,uint256,uint256)",
                _fundingCId,
                _amount,
                _duration
            )
      );

abi.encodeWithSignature creates a function selector that is used to execute the initialize function. The initialize function accepts as arguments variables of string, uint256 and uint256. Remember that we pass these three variables to the createCrowdFundingContract function.

We perform a check to find out if the function call was successful before we push the address of the clone contract into the _deployedContract array and emit the newCrowdFundingCreated event.

Withdrawing Funds From the Factory Contract

The contract owner can withdraw the Ether in the factory contract. Only the contract owner can call the withdrawFunds function. The onlyOwner modifier is from the Ownable contract. It checks if the caller msg.sender is the contract owner. The owner can only withdraw if there is Ether inside the contract. We use the low-level call method to withdraw the Ether.

  function withdrawFunds() public onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "nothing to withdraw");
        (bool success, ) = payable(msg.sender).call{value: balance}("");
        require(success, "withdrawal failed");
    }

The receive function is necessary so the contract can receive Ether.

 receive() external payable {}

Implementing the Crowd Funding Contract

Open the CrowdFundingContract.sol file and create the basic skeleton for the contract.

//SPDX-License-Identifier:MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/proxy/utils/Initializable.sol";

 contract CrowdFundingContract is Initializable {

    //state variable
    address payable private _campaignOwner;
    string public fundingId;
    uint256 public targetAmount;
    uint256 public campaignDuration;

    function initialize(
        string calldata _fundingId,
        uint256 _amount,
        uint256 _duration
    ) external initializer {
        _campaignOwner = payable(tx.origin);
        fundingId = _fundingId;
        targetAmount = _amount;
        campaignDuration = _duration;
    }

}

At the top of the file, we import the Initializable contract from Openzeppelin. The CrowdFundingContract contract derives from the Initializable contract, which allows us to use the initializer modifier. The initialize function acts like a constructor; we can only call it once. Remember that when we create a new instance of the CrowdFundingContract, we call the initialize function.

Inside the initialize function, we set the values of some state variables. The _fundingCId is the IPFS hash that contains details about the crowdfunding project; the targetAmount and campaignDuration are also set. The _campaignOwner value equals tx.origin.

The address that initiates the transaction is tx.origin. A contract may have some internal transactions, and in our case, we are calling the initialize contract from within the createCrowdFundingContract function of the CrowdSourcingFactory contract, msg.sender, which generally equates to the caller of the function, will be the CrowdSourcingFactory contract rather than the wallet owner that called the function. So, tx.origin gives us the wallet address that started the transaction.

Functions in the Crowd Funding Contract

A crowdfunding contract needs a function to allow people to donate to the cause.

Creating the makeDonation Function

//other part of the contract

bool public campaignEnded;
uint256 private _numberOfWithdrawal;
uint256 private _numberOfDonors;
uint256 private _amountDonated;

mapping(address => uint256) public donors;

//event
event fundsDonated(address indexed donor, uint256 amount, uint256 date);

 function makeDonation() public payable {
        uint256 funds = msg.value;
        require(!campaignEnded, "campaign ended");
        require(funds > 0, "You did not donate");
        require(_numberOfWithdrawal != 3, "no longer taking donation");
        if (donors[msg.sender] == 0) {
            _numberOfDonors += 1;
        }

        donors[msg.sender] += funds;
        _amountDonated += funds;
        emit fundsDonated(msg.sender, funds, block.timestamp);
    }

The makeDonation function is payable, so the function's caller needs to send Ether to the contract before calling it. We perform a check to see if the campaign has ended. The campaignEnded variable is true when the contract owner successfully withdraws funds from the contract three times.

We have a mapping of address to uint256 called donors. We use the donors mapping to save and track the donation made by people to the contract. We increment the _numberOfDonors by one only if the address hasn't donated before. We also increment the _amountDonated by the value of msg.value (Ether amount sent by the caller). At the end of the function, we emit the event fundsDonated.

Creating the Campaign Milestone Function

A campaign owner creates a milestone that donors to the campaign vote on. We save the milestone information in IPFS and the resulting CID on-chain. Below is the code used to create a milestone.

 emit milestoneCreated(msg.sender, block.timestamp, votingPeriod);

enum MilestoneStatus {
    Approved,
    Declined,
    Pending
}

contract CrowdFundingContract is Initializable {
     //other parts of the code

uint32 private _milestoneCounter;
uint256 private _numberOfWithdrawal;

    struct Milestone {
          string milestoneCID;
          bool approved;
          uint256 votingPeriod;
          MilestoneStatus status;
          MilestoneVote[] votes;
      }

      struct MilestoneVote {
              address donorAddress;
              bool vote;
       }
    mapping(uint256 => Milestone) public milestones;

//event
event milestoneCreated(address indexed owner, uint256 datecreated, uint256 period);


  function creatNewMilestone(string memory milestoneCID, uint256 votingPeriod)
          public
      {
          require(msg.sender == _campaignOwner, "you not the owner");
          //check if we have a pending milestone
          //check if we have a pending milestone or no milestone at all
          require(
              milestones[_milestoneCounter].status != MilestoneStatus.Pending,
              "you have a pending milestone"
          );

          //check if all three milestone has been withdrawn
          require(_numberOfWithdrawal != 3, "no more milestone to create");

          //create a new milestone increment the milestonecounter
          _milestoneCounter++;

          //voting period for a minimum of 2 weeks before the proposal fails or passes
          Milestone storage new milestone = milestones[_milestoneCounter];
          new milestone.milestoneCID = milestoneCID;
          new milestone.approved = false;
          new milestone.votingPeriod = votingPeriod;
          new milestone.status = MilestoneStatus.Pending;
          emit milestoneCreated(msg.sender, block.timestamp, votingPeriod);
      }

}

Outside the contract, we created an enum called MilestoneStatus. We determine the state of a milestone by the enum. Enums are user-defined types that limit the MilestoneStatus value to Approved, Declined, and Pending. We also made a struct called Milestone. We will use this struct data type to save all milestones created. It consists of a milestoneCID, the CID of the milestone details data saved on IPFS.

 struct Milestone {
          string milestoneCID;
          bool approved;
          uint256 votingPeriod;
          MilestoneStatus status;
          MilestoneVote[] votes;
      }

It also has an array of MilestoneVote. The MilestoneVote is a struct that holds the vote of each donor on a created milestone.

    struct MilestoneVote {
              address donorAddress;
              bool vote;
       }

The createNewMilestone function takes a string data milestoneCID and a uint256 votingPeriod value as parameters. The votingPeriod is the time frame during which donors can vote on a milestone. If the period has passed and a donor has not voted, we consider that donor to have voted for the milestone. The milestone creator can set the voting period for convenience.

We performed checks inside the function first to see if the function's caller is the _campaignOwner and to ensure that the contract owner has not made more than three withdrawals from the contract. We want the maximum successful withdrawal to be 3.

 require(_numberOfWithdrawal != 3, "no more milestone to create");

If both checks pass, we increment the _milestoneCounter state variable by 1. Each created milestone is stored in a mapping of uint256 => Milestone.

  mapping(uint256 => Milestone) public milestones;

We create a storage variable newmilestone and save it in the location of the milestones[_milestoneCounter].

  Milestone storage newmilestone = milestones[_milestoneCounter];
  newmilestone.milestoneCID = milestoneCID;
  newmilestone.approved = false;
  newmilestone.votingPeriod = votingPeriod;
  newmilestone.status = MilestoneStatus.Pending;
  emit milestoneCreated(msg.sender, block.timestamp, votingPeriod);

A created milestone starts in the MilestoneStatus.Pending state. We emit an event at the end of the function.

Creating the Vote on a Milestone Function

A donor to a campaign can vote on a milestone within the voting period allowed. The voteOnMilestone function receives a bool as a parameter. We update the user vote according to the milestone struct.

 function voteOnMilestone(bool vote) public {
        //check if the milestone is pending, which means we can vote
        require(
            milestones[_milestoneCounter].status == MilestoneStatus.Pending,
            "can not vote on milestone"
        );
        //check if the person has voted already
        //milestone.votes

        //check if this person is a donor to the cause
        require(donors[msg.sender] != 0, "you are not a donor");

        uint256 counter = 0;
        uint256 milestoneVoteArrayLength = milestones[_milestoneCounter]
            .votes
            .length;
        bool voted = false;
        for (counter; counter < milestoneVoteArrayLength; ++counter) {
            MilestoneVote memory userVote = milestones[_milestoneCounter].votes[
                counter
            ];
            if (userVote.donorAddress == msg.sender) {
                //already voted
                voted = true;
                break;
            }
        }
        if (!voted) {
            //the user has not voted yet
            MilestoneVote memory userVote;
            //construct the user vote
            userVote.donorAddress = msg.sender;
            userVote.vote = vote;
            milestones[_milestoneCounter].votes.push(userVote);

        }
    }

We first determine whether the milestone on which the user is voting is in the Pending state. We accomplish this by retrieving the current milestone's status. _milestoneCounter is a state variable that keeps track of the current milestone. Because only one milestone can be active at any given time, the _milestoneCounter will always point to the most recent milestone.

   require(milestones[_milestoneCounter].status == MilestoneStatus.Pending,
           "can not vote on milestone"
        );

We also check if the user address is a donor by checking the donors mapping to get the amount donated. If the amount is zero, they have not donated. We looped through the votes array of the milestone struct to check if the user has voted before.

   bool voted = false;
        for (counter; counter < milestoneVoteArrayLength; ++counter) {
            MilestoneVote memory userVote = milestones[_milestoneCounter].votes[
                counter
            ];
            if (userVote.donorAddress == msg.sender) {
                //already voted
                voted = true;
                break;
              }
        }

If the user has voted, we break and exist from the for loop, but if the user is yet to vote:

 if (!voted) {
       //the user has not voted yet
       MilestoneVote memory userVote;
       //construct the user vote
       userVote.donorAddress = msg.sender;
       userVote.vote = vote;
       milestones[_milestoneCounter].votes.push(userVote);
     }

We initialize a MilestoneVote'struct in memory and fill it with the user address and vote choice. We push the createdMilestoneinto the currentmilestones`votes array field, which is of type MilestoneVote[].

Creating the Withdraw Milestone Funds Function

According to the business rules, a campaign owner can only withdraw funds from the contract after three successful milestones. If the voting period has expired, you may withdraw the funds designated for the milestone. We tally the total number of donors' votes, and if the yes vote equals or exceeds two-thirds of the total number of donors, the milestone is met, and the Ether for that milestone is transferred to the campaign owner.

  function withdrawMilestone() public {

        require(payable(msg.sender) == _campaignOwner, "you not the owner");

        //check if the voting period is still on
        require(
            block.timestamp > milestones[_milestoneCounter].votingPeriod,
            "voting still on"
        );
        //check if milestone has ended
        require(
            milestones[_milestoneCounter].status == MilestoneStatus.Pending,
            "milestone ended"
        );

        //calculate the percentage
        (uint yesvote, uint256 novote) = _calculateTheVote(
            milestones[_milestoneCounter].votes
        );

        //calculate the vote percentage and make room for those that did not vote
        uint256 totalYesVote = _numberOfDonors - novote;

        //check if the yesVote is equal to 2/3 of the total votes
        uint256  twoThirdofTotal  = (2 * _numberOfDonors * _baseNumber) / 3;
        uint256 yesVoteCalculation = totalYesVote * _baseNumber;

        //check if the milestone passed 2/3
        if (yesVoteCalculation >=  twoThirdofTotal ) {
            //the milestone succeeds payout the money
            milestones[_milestoneCounter].approved = true;
            _numberOfWithdrawal++;
            milestones[_milestoneCounter].status = MilestoneStatus.Approved;
            //transfer 1/3 of the total balance of the contract
            uint256 contractBalance = address(this).balance;
            require(contractBalance > 0, "nothing to withdraw");
            uint256 amountToWithdraw;
            if (_numberOfWithdrawal == 1) {
                //divide by 3 1/3
                amountToWithdraw = contractBalance / 3;
            } else if (_numberOfWithdrawal == 2) {
                //second withdrawal 1/2
                amountToWithdraw = contractBalance / 2;
            } else {
                //final withdrawal
                amountToWithdraw = contractBalance;
                campaignEnded = true;
            }

            (bool success, ) = _campaignOwner.call{value: amountToWithdraw}("");
            emit fundsWithdrawn(
                _campaignOwner,
                amountToWithdraw,
                block.timestamp
            );
            require(success, "withdrawal failed");
        } else {
            //the milestone failed
            milestones[_milestoneCounter].status = MilestoneStatus.Declined;
            emit milestoneRejected(yesvote, novote);
        }
    }

    function _calculateTheVote(MilestoneVote[] memory votesArray)
        private
        pure
        returns (uint256, uint256)
    {
        uint256 yesNumber = 0;
        uint256 noNumber = 0;
        uint256 arrayLength = votesArray.length;
        uint256 counter = 0;

        for (counter; counter < arrayLength; ++counter) {
            if (votesArray[counter].vote == true) {
                ++yesNumber;
            } else {
                ++noNumber;
            }
        }

        return (yesNumber, noNumber);
    }

The function determines whether the caller is the _campaignOwner and whether the milestone's voting period is still active. If both checks pass, the function checks the milestone's status to ensure it is pending. We define a private function _calculateTheVote that takes an array of MilestoneVote as a parameter.

The function _calculateTheVote compiles the votes and returns a tuple of yes and no votes. To calculate the total number of those who voted yes for the milestone, we subtract the no votes from the _numberOfDonors state variable (those that do not vote are assumed to have voted for the milestone).

We need a two-thirds yes vote to pass the milestone. We multiply the total number of donors by a _baseNumber that equals '10 ** 18`. We do this because our sample donor size may be small and to avoid rounding errors. We followed the same procedure for the yes vote.

  uint256 twoThirdofTotal = (2 * _numberOfDonors * _baseNumber) / 3;
  uint256 yesVoteCalculation = totalYesVote * _baseNumber;

If the yesVoteCalculation is greater or equal to `twoThirdofTotal, that means the milestone passes. We calculate the total balance of the contract and disburse it based on the milestone withdrawal.

  if (_numberOfWithdrawal == 1) {
                //divide by 3 1/3
                amountToWithdraw = contractBalance / 3;
            } else if (_numberOfWithdrawal == 2) {
                //second withdrawal 1/2
                amountToWithdraw = contractBalance / 2;
            } else {
                //final withdrawal
                amountToWithdraw = contractBalance;
                campaignEnded = true;
            }

If this is the first withdrawal, the available contract balance is divided by three; if this is the second withdrawal, it is divided by two; in the final withdrawal, all Ether is sent to the caller.

The calculated milestone amount is transferred from the contract using the low-level call method. A fundsWithdraw event is emitted on a successful transfer.

    (bool success, ) = _campaignOwner.call{value: amountToWithdraw}("");
     require(success, "withdrawal failed");

Milestone rejected scenario; If the yesVoteCalculation is less than twoThirdofTotal, the milestone fails, and we set the status of the milestone to Declined and emit an event.

//milestone failed
 milestones[_milestoneCounter].status = MilestoneStatus.Declined;
 emit milestoneRejected(yesvote, novote);

Creating Utility Functions

We can use other functions in the crowdfunding contract to retrieve the values of private variables.

   function getDonation() public view returns (uint256) {
        return _amountDonated;
    }

    function campaignOwner() public view returns (address payable) {
        return _campaignOwner;
    }

    function numberOfDonors() public view returns (uint256) {
        return _numberOfDonors;
    }

    function showCurrentMillestone() public view returns (Milestone memory) {
        return milestones[_milestoneCounter];
    }

Creating Unit Tests for the Smart Contract

Hardhat provides some wonderful helpers we can use when unit-testing our smart contract code. The unit test is inside the test folder of the project. At the top of the file, we required some helpers from hardhat.

const { time,  loadFixture } = require("@nomicfoundation/hardhat-network-helpers");

The time helper is used to manipulate blockchain time, whereas the loadFixture executes a setup function only once when called. On subsequent calls, It returns the snapshot or result of the setup function instead of re-executing it.

 describe("CrowdFunding", function () {
      async function setUpContractUtils() {
     //1.
    const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
    const DEPOSIT = ethers.utils.parseEther("0.001");
    const FUTURE TIME = (await time.latest()) + ONE_YEAR_IN_SECS;
    let fundingCId =
      "bafybeibhwfzx6oo5rymsxmkdxpmkfwyvbjrrwcl7cekmbzlupmp5ypkyfi";
    let milestoneCID =
      "bafybeibhwfzx6oo5rymsxmkdxpmkfwyvbjrrwcl7cekmbzlupmp5ypkyfi";
      // Contracts are deployed using the first signer/account by default
      const [
        owner,
        otherAccount,
        someOtherAccount,
        accountOne,
        accountTwo,
        accountThree,
        accountFour,
      ] = await ethers.getSigners();

      //2.deploy the contracts here
      const CrowdFundingImplementation = await hre.ethers.getContractFactory(
        "CrowdFundingContract"
      );
      const crowdFundingImplementation =
        await CrowdFundingImplementation.deploy();
      await crowdFundingImplementation.deployed();

      //3. deploy the factory contract
      const CrowdFundingFactory = await hre.ethers.getContractFactory(
        "CrowdSourcingFactory"
      );

      const crowdFundingFactory = await CrowdFundingFactory.deploy(
        crowdFundingImplementation.address
      );
      await crowdFundingFactory.deployed();

      //4 deploy a new crowdfunding contract
      let txn = await crowdFundingFactory
        .connect(otherAccount)
        .createCrowdFundingContract(fundingCId,  DEPOSIT, FUTURE TIME, {
          value:  DEPOSIT,
        });


      let wait = await txn.wait();
      const cloneAddress = wait.events[1].args.cloneAddress;

      //5. use the clone 
      let instanceOne = await hre.ethers.getContractAt(
        "CrowdFundingContract",
        cloneAddress,
        otherAccount
      );

      let txnTwo = await crowdFundingFactory
        .connect(otherAccount)
        .createCrowdFundingContract(fundingId,  DEPOSIT, FUTURETIME, {
          value:  DEPOSIT,
        });

      let waitTwo = await txnTwo.wait();
      const cloneAddressTwo = waitTwo.events[1].args.cloneAddress;


      let instanceTwo = await hre.ethers.getContractAt(
        "CrowdFundingContract",
        cloneAddressTwo,
        someOtherAccount
      );

      return {
        FUTURE TIME,
        DEPOSIT,
        owner,
        otherAccount,
        someOtherAccount,
        contractFactory: crowdFundingFactory,
        fundingId,
        amountToDeposit,
        instanceOne,
        instanceTwo,
        milestoneCID,
        accountOne,
        accountTwo,
        accountThree,
        accountFour,
      };
  }

  }

We defined an async function setUpContractUtils which will be passed to the loadFixtures. The setUpContractUtils functions deploys the crowdfunding contract and the contract factory. We returned an object of contract instances used to run our test. You can find the complete test in the test folder inside the project directory.

The code above sets up and deploys the implementation contract (crowdfunding contract) and the factory contract. Let us break it down using the number in the comment to explain each code section.

  1. At the top of the function, we define some constant variables. Using the time method provided by hardhat, we defined a FUTURE TIME time variable which is the sum of the current blockchain time and a year in seconds. We also defined 'fundingCId' and'milestoneCId' variables, representing the IPFS content hash of the data stored off-chain for our purposes.
  2. The crowdfunding contract (implementation contract) is deployed.
  3. We deployed the factory contract passing in the address of the previously deployed smart contract. (the address of the deployed crowdfunding contract). Remember, we will use the factory to create clones of the implementation contract.
  4. We deploy a new instance of the crowdfunding contract.

          let txn = await crowdFundingFactory.connect(otherAccount)
                 .createCrowdFundingContract(fundingCId,  DEPOSIT, FUTURETIME, {
           value:  DEPOSIT,
         });
    

    We connect to the crowdFundingFactory contract instance with the signer otherAccount and call the createCrowdFundingContract passing in thefundingCID,
    DEPOSIT and FUTURE TIME, represent the IPFS hash, the amount we are raising, and the duration of the crowdfunding campaign. We also send Ether to the factory method that will create the clone.

    We await the transaction and retrieve the address of the clone from the factory contract event.

        let wait = await txn.wait();
       const cloneAddress = wait.events[1].args.cloneAddress;
    
  5. We use the new clone address to retrieve an instance of newly created crowdfunding contract.

      let instanceOne = await hre.ethers.getContractAt("CrowdFundingContract",
         cloneAddress,
         otherAccount
       );
    

    To make use of any variable exported from the setUpContractUtils function we await loadFixture(setUpContractUtils)

  const { instanceOne, otherAccount, someOtherAccount } = 
   await loadFixture(setUpContractUtils);

To run our test :

npm run test

Writing a Deployment Script

We are going to write a deploy script. Open the scripts folder and create a new file called deploy.js. We should deploy our implementation contract first before the factory contract. The factory contract creates a clone of the implementation contract.


const hre = require("hardhat");

async function main() {
  //deploy the crowdfunding contract implementation 
    const CrowdFundingImplementation = await       
    hre.ethers.getContractFactory("CrowdFundingContract");
    console.log("deploying the implementation contract")
    const crowdFundingImplementation = await CrowdFundingImplementation.deploy();
    await crowdFundingImplementation.deployed();
    console.log("deployed the implementation contract with address : ", 
   crowdFundingImplementation.address);
    //create the factory contract
    const CrowdFundingFactory = await 
   hre.ethers.getContractFactory("CrowdSourcingFactory");
    const crowdFundingFactory = await 
    CrowdFundingFactory.deploy(crowdFundingImplementation.address);
    console.log("deployed the factory contract");
    await crowdFundingFactory.deployed();
    console.log("deployed the factory contract with to : ", crowdFundingFactory.address);
}

main().catch((error) => {
  console.error("There was an error",error);
  process.exitCode = 1;
});

To run the deployment on the local hardhat network, we run this command on the terminal:

   npm run deploy-local

Deploying to the Mumbai network, we run this command on the terminal:

npm run deploy

The commands are set up in the package.json file.

In the Github project, there is a run.js script you can execute by running :

  npm run run-local

Conclusion

In this tutorial, we learned how to create a crowdfunding contract. We implemented the Openzeppelin Clones using a factory contract to create clones from an implementation contract. We also deployed and tested the smart contract. In the next section of the series, we will create and deploy the crowdfunding contract off-chain data to IPFS.

Thanks for reading.

 
Share this