Writing a crowdfunding smart contract on EVM chains using Solidity

Writing a crowdfunding smart contract on EVM chains using Solidity

To better write Solidity code, creating projects with some utility is often advised. In this article, we will write a crowdfunding smart contract that allows the contract deployer to gather funds for an initiative they wish to raise. For this contract, we will input the deadline and target funds through the constructor by the contract deployer.

When the deadline is crossed, the fund will be disabled, and nobody can fund the contract. At that point, if the target funds are achieved, the owner can withdraw all the funds. If not, the funders can get their funding back, as the target funds were not achieved until the provided deadline. In such a case, the contract owner cannot withdraw the funds.

If the target funds are achieved before the deadline, the contract can continue receiving funds until the deadline. However, the owner can withdraw the funds before the deadline. If the owner chooses to withdraw before the deadline, the fund is closed and won't accept any further funding.

Now that we have taken a brief look at how the contract will work let's start working on the contract!

I've got you covered if you prefer video tutorials over written ones! I've uploaded a video on the same topic, and we deploy and test the features in the contract there. Watch the video now!

Setting up the development environment

Creating a hardhat project

We will use hardhat in this tutorial to compile and manage our contract. Navigate to a safe directory, and run the following command to initialize the hardhat project initialization wizard:

npx hardhat

This should load up the initializer. Choose your configuration; in this case, I'll be choosing JavaScript (although we won't be writing scripts in this tutorial, although if you wish to do so, this choice might be important for you). Finally, choose to install the dependencies for your hardhat project.

Creating the contract file

Hardhat creates a file named Lock.sol within the contracts folder with some example code for you. You can delete the file and create a new file named Crowdfunding.sol. Then you can initialize a simple contract.

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

contract Crowdfunding {

}

Writing the contract

Defining the states

Our contract will have states that will store important data about the funders and the contract owner.

mapping(address => uint256) public funders;
uint256 public deadline;
uint256 public targetFunds;
string public name;
address public owner;
bool public fundsWithdrawn;

Let's look at the purpose of each state variable:

  • funders: a mapping that stores the amount of funds each address has contributed.

  • deadline: the date and time at which funding will no longer be accepted.

  • targetFunds: the amount of funds that the project hopes to raise.

  • name: the name of the project being funded.

  • owner: the address of the owner of the contract.

fundsWithdrawn: a flag that is set to true when the owner withdraws the funds.

Defining the events

Our contract will emit events that other applications can listen to and track any changes in the contract:

event Funded(address _funder, uint256 _amount);
event OwnerWithdraw(uint256 _amount);
event FunderWithdraw(address _funder, uint256 _amount);

Let's look at the purpose of each event:

  • Funded: triggered when someone contributes funds to the contract. It takes two parameters- the address of the funder and the amount contributed.

  • OwnerWithdraw: triggered when the owner withdraws the funds from the contract. It takes one argument: the amount of funds withdrawn.

  • FunderWithdraw: triggered when a funder withdraws funds from the contract. It takes two arguments: the address of the funder and the amount of funds withdrawn.

Writing the constructor

The following is the code for the constructor of this contract:

constructor(string memory _name, uint256 _targetFunds, uint256 _deadline) {
    owner = msg.sender;
    name = _name;
    targetFunds = _targetFunds;
    deadline = _deadline;
}

The constructor is called when the contract is deployed. It takes three arguments: the project's name, the target funds, and the deadline. It sets the owner of the contract to the address that deployed the contract and sets the project name, target funds, and deadline.

Writing the helper functions

These helper functions will help us get the current status of the contract. Following is the code for the isFundEnabled() function:

function isFundEnabled() public view returns(bool) {
    if (block.timestamp > deadline || fundsWithdrawn) {
        return false;
    } else {
        return true;
    }
}

This function is a helper function that returns a Boolean value indicating if funding is still enabled. It checks if the current block timestamp is greater than the deadline or if the funds have been withdrawn.

The following is the code for isFundSuccess() function:

function isFundSuccess() public view returns(bool) {
    if(address(this).balance >= targetFunds || fundsWithdrawn) {
        return true;
    } else {
        return false;
    }
}

This function is a helper function that returns a Boolean value indicating if the funding goal has been reached. It checks if the contract balance is greater than or equal to the target funds or if the funds have been withdrawn.

Writing the fund() function

The following code is for the fund() function:

function fund() public payable {
    require(isFundEnabled() == true, "Funding is now disabled!");

    funders[msg.sender] += msg.value;
    emit Funded(msg.sender, msg.value);
}

The fund() function allows someone to contribute funds to the contract. It has a require statement that checks if funding is still enabled before allowing the funds to be contributed. If funding is enabled, the function adds the amount of the contribution to the funder's total in the mapping and triggers the Funded event.

Writing the withdrawOwner() function

The following code is for the withdrawOwner() function:

function withdrawOwner() public {
    require(msg.sender == owner, "Not authorized!");
    require(isFundSuccess() == true, "Cannot withdraw!");

    uint256 amountToSend = address(this).balance;
    (bool success,) = msg.sender.call{value: amountToSend}("");
    require(success, "unable to send!");
    fundsWithdrawn = true;
    emit OwnerWithdraw(amountToSend);
}

The withdrawOwner() function allows the contract owner to withdraw the funds from the contract. It has a require statement that checks if the msg.sender is the owner and if the funding goal has been reached before allowing the funds to be withdrawn. If the funding goal has been reached and the msg.sender is the owner, the function sends the balance of the contract to the owner address and triggers the OwnerWithdraw event.

Writing the withdrawFunder() function

The following code is for the withdrawFunder() function:

function withdrawFunder() public {
    require(isFundEnabled() == false && isFundSuccess() == false, "Not eligible!");

    uint256 amountToSend = funders[msg.sender];
    (bool success,) = msg.sender.call{value: amountToSend}("");
    require(success, "unable to send!");
    funders[msg.sender] = 0;
    emit FunderWithdraw(msg.sender, amountToSend);
}

The withdrawFunder() function allows a funder to withdraw funds from the contract. It has a require statement that checks if funding is still enabled and if the funding goal has been reached before allowing the funds to be withdrawn. If funding is not enabled and the funding goal has not been reached, the function sends the funder's contribution to the funder address and triggers the FunderWithdraw event.

Complete code

The following is the complete code for the Crowdfunding contract:

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

contract Crowdfunding {
    mapping(address => uint256) public funders;
    uint256 public deadline;
    uint256 public targetFunds;
    string public name;
    address public owner;
    bool public fundsWithdrawn;

    event Funded(address _funder, uint256 _amount);
    event OwnerWithdraw(uint256 _amount);
    event FunderWithdraw(address _funder, uint256 _amount);

    constructor(string memory _name, uint256 _targetFunds, uint256 _deadline) {
        owner = msg.sender;
        name = _name;
        targetFunds = _targetFunds;
        deadline = _deadline;
    }

    function fund() public payable {
        require(isFundEnabled() == true, "Funding is now disabled!");

        funders[msg.sender] += msg.value;
        emit Funded(msg.sender, msg.value);
    }

    function withdrawOwner() public {
        require(msg.sender == owner, "Not authorized!");
        require(isFundSuccess() == true, "Cannot withdraw!");

        uint256 amountToSend = address(this).balance;
        (bool success,) = msg.sender.call{value: amountToSend}("");
        require(success, "unable to send!");
        fundsWithdrawn = true;
        emit OwnerWithdraw(amountToSend);
    }

    function withdrawFunder() public {
        require(isFundEnabled() == false && isFundSuccess() == false, "Not eligible!");

        uint256 amountToSend = funders[msg.sender];
        (bool success,) = msg.sender.call{value: amountToSend}("");
        require(success, "unable to send!");
        funders[msg.sender] = 0;
        emit FunderWithdraw(msg.sender, amountToSend);
    }

    // Helper functions, although public

    function isFundEnabled() public view returns(bool) {
        if (block.timestamp > deadline || fundsWithdrawn) {
            return false;
        } else {
            return true;
        }
    }

    function isFundSuccess() public view returns(bool) {
        if(address(this).balance >= targetFunds || fundsWithdrawn) {
            return true;
        } else {
            return false;
        }
    }
}

Conclusion

This article taught us how to write your crowdfunding contract using Solidity. You can now also wrap this around a contract factory and enable others to deploy these contracts. But that's for another tutorial.

As mentioned, you can check out the video tutorial on the same topic on my YouTube channel if you want me to test this contract or if you prefer video tutorials.