DEV Community

Jamiebones
Jamiebones

Posted on

Creating A Crowd Sourcing Solidity Contract

Description

The code for this post is available here

This blog post will walk you through how to create a crowd sourcing smart contract. The implementation consist of two smart contracts which are the main smart contract implementation called CrowdSourcing.sol and the factory used to create other proxies from the main contract called CrowdSourcingFactory.sol. There is also an interface called ICrowdSourcing.sol that has some methods signature.

The project

The project was set up using hardhat and selecting the advanced project setup with Typescript.

Code

EIP-1167 minimal proxies of Ethereum proposed how to save gas when deploying the same contract on the blockchain. The contract crowdSourcing is the main contract which is deployed first and its deployed address is attached to the factory contract (crowdSourcingFactory) to use to create proxy contracts off the main contract. The code makes use of Openzepplein Clones library to create clones of the deployed crowd sourcing contract.

A schematic representation:

Diagram showing proxy contract

A user who wants to create a new crowd sourcing contract calls the contract factory which creates new proxy contracts with their own state. This is done by delegateCall. The crowd sourcing master contract is firstly deployed and its address is passed on to the factory contract that will create clone of the master contract.

crowdSourcing.sol

  //SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/math/Math.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract CrowdSourcing is Ownable, Initializable {
    string public purpose;
    uint256 public targetAmount;
    uint256 public amountDonated;
    mapping(address => uint256) public donors;
    address[] public donorsAddress;
    address public deployer;
    bool public campaignRunning;

    //Events
    event DonationMade(address indexed donorAddress, uint256 amount, string indexed crowdsource );
    event DonationWithdrawn(address indexed receipient, uint256 amount, string indexed crowdsource);

    struct DonorsAmount {
        address donorsAddress;
        uint256 amount;
    }

    function initialize(string memory _purpose, uint256 _targetamount, address _deployer) public initializer {
        purpose = _purpose;
        targetAmount = _targetamount;
        campaignRunning = true;
        deployer = _deployer;

        //Ownable.initialize(msg.sender);
    }

    function donateToCause() public payable isCampaignOn   {
      require(msg.value > 0, "donation of 0 ether made");
      require(msg.sender != address(this), "contract cannot make donation");
      //add the amount to the amountDonated
      amountDonated += (msg.value);
      //check if we have a donation made before
      bool exists = donors[msg.sender] != 0;
       //include addres and amount in the mapping donors
      donors[msg.sender] += msg.value;
      if ( !exists ){
          //push address to the donors addresss
          donorsAddress.push(msg.sender);
      }

      //emit a Donation Made event
      emit DonationMade(msg.sender, msg.value, purpose);
    }

    function withdrwaDonation() public payable isCampaignOn isDeployer {
        require(amountDonated > 0, "Nothing to withdraw");
        campaignRunning = false;
        payable(address(msg.sender)).transfer(amountDonated);
        emit DonationWithdrawn(msg.sender, amountDonated, purpose);
    }

    function getDonorsList() public view returns (DonorsAmount[] memory){
        uint256 totalDonors = donorsAddress.length;
        DonorsAmount[] memory donorsArray = new DonorsAmount[](totalDonors);
        for (uint256 i=0; i < totalDonors; i++){
            address currentAddress = donorsAddress[i];
            uint256 amount = donors[currentAddress];
            donorsArray[i] = DonorsAmount(
                currentAddress,
                amount
            );
        }
        return donorsArray;
    }


    modifier isCampaignOn {
        require(campaignRunning == true, "Campaign is not running");
        _;
    }

    modifier isDeployer{
        require(deployer == msg.sender, "caller not deployer");
        _;
    }
}
Enter fullscreen mode Exit fullscreen mode

The contract declares some state variable for storing data that is peculiar to each created proxy contract.

variables in contract

purpose : to store the purpose for the crowd funding
targetAmount : to store the amount hoped to raised
amountDonated : to store how much has being donated
donors : a mapping used to store the address of donors and amount donated
donorsAddress : to store the address of each donor. This is useful because we want the ability to be able to pull out all the donors and their donated amount. See Iterable map pattern
deployer : to store the person that deployed the contract
campaignRuning : a boolean to check if the campaign is running
DonationMade : event that is fired when a donation is made
DonationWithdrawn : event that is emitted when the donation is withdrawn

Contract function

initialize

The contract has an initialize function that is called when the contract is created by the factory. The initialize function acts like a constructor as it is called once. OpenZeppelin Initializable contract which is inherited ensures that the initialize function is only called once. The deployer variable is used to store the address of the person that deploys the contract.

donateToCause

This function is called when someone makes a donation to your cause. It stores the address of the donor and the amount in the donors variable. A DonationMade event is emitted on a successful donation.

withdrwaDonation

This function can only be called by the owner of the crowd sourcing contract to withdraw the donation made. The DonationWithdrawn event is emitted on a successful withdrawal.

getDonorsList

This function is used to get the list of donors.

crowdSourcingFactory.sol

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

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


contract CrowdSourcingFactory is Ownable {
    address public implementation;
    address[] public allCrowdSource;
    mapping(bytes32 => address ) private idToAddress;


    constructor(address _implementation) {
        implementation = _implementation;
    }

    function createCrowdSourceContract(string memory _purpose, uint256 _targetamount, address _deployer) 
    external payable returns (address crowdContract){
        bytes32 id = _getOptionId(_purpose, _targetamount);
        require(idToAddress[id] == address(0), "Crowd sourcing type exist");
        bytes32 salt = keccak256(abi.encodePacked(_purpose, _targetamount));
        crowdContract = Clones.cloneDeterministic(implementation, salt);
        ICrowdSourcing(crowdContract).initialize(_purpose, _targetamount, _deployer);
        allCrowdSource.push(crowdContract);
        idToAddress[id] = crowdContract;
    }

    function getCrowdSource(string memory _purpose, uint256 _amount ) public view returns (address){
        bytes32 id = _getOptionId(_purpose, _amount);
        return idToAddress[id];
    }

    function _getOptionId(string memory _purpose, uint256 amount) internal pure returns (bytes32){
        return keccak256(
            abi.encodePacked(_purpose, amount)
        );
    }

    function getNumberofCloneMade() public view returns (uint256) {
        return allCrowdSource.length;
    }
}

Enter fullscreen mode Exit fullscreen mode

variables in contract factory

implementation : this stores the address of the master contract (CrowdSourcing)
allCrowdSource : this stores the address of all the created proxy contract
idToAddress : this stores a mapping in bytes of the contract purpose and the targeted amount.

Contract functions

constructor

This sets the address of the master contract in the factory contract. The factory creates proxies from the master.

createCrowdSourceContract

This function accepts three arguments which are the purpose of the crowd sourcing, the targeted amount and the address of the deployer. This function is used to create a proxy contract. The function firstly checked if an exact contract with the same name and target sum already exist. It does this by an internal function called _getOptionId If a contract with such parameters does not exists, it encodes the purpose and target sum of the contract into bytes. The factory uses the Clones contract imported from Openzeppelin to create clones by passing address of the already deployed master contract and the encoded bytes to the Clones.cloneDeterministic function which returns the address of the proxy contract.

crowdContract = Clones.cloneDeterministic(implementation, salt);
crowdContract is the address of the created proxy.
ICrowdSourcing(crowdContract).initialize(_purpose, _targetamount, _deployer);
We imported the interface ICrowdSourcing and used it with the created proxy address by calling the function initialize with the three parameters which are : purpose, targetAmount and deployer.

I noticed that calling msg.sender in the initialize function equated to the zero address. That's why I explicitly passed the address of the deployer.

getCrowdsource

This is used to get the address of the deployed proxy contract by passing in the purpose of the contract and the target amount hoping to raise.

getNumberofCloneMade

This function returns the number of clones made. It reads and returns the length of the allCrowdSource array.

Summary

The EIP-1167 minimal proxy is the cheapest way to clone contract and this post has talked about how that can be done. Thanks for reading.

Top comments (0)