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:
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");
_;
}
}
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;
}
}
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 theinitialize
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)