Crowdfund

Warning

This is example code for learning purposes. Do not use in production without thorough review and testing.

Now, let’s explore a straightforward example for a crowdfunding contract where prospective participants can contribute funds to a campaign. If the total contribution to the campaign reaches or surpasses a predetermined funding goal, the funds will be sent to the beneficiary at the end of the campaign deadline. Participants will be refunded their respective contributions if the total funding does not reach its target goal.

 1#pragma version >0.3.10
 2
 3###########################################################################
 4## THIS IS EXAMPLE CODE, NOT MEANT TO BE USED IN PRODUCTION! CAVEAT EMPTOR!
 5###########################################################################
 6
 7# example of a crowd funding contract
 8
 9funders: HashMap[address, uint256]
10beneficiary: address
11deadline: public(uint256)
12goal: public(uint256)
13timelimit: public(uint256)
14finalized: bool
15
16# Setup global variables
17@deploy
18def __init__(_beneficiary: address, _goal: uint256, _timelimit: uint256):
19    self.beneficiary = _beneficiary
20    self.deadline = block.timestamp + _timelimit
21    self.timelimit = _timelimit
22    assert _goal > 0, "Goal must be non-zero"
23    self.goal = _goal
24
25# Participate in this crowdfunding campaign
26@external
27@payable
28def participate():
29    assert block.timestamp < self.deadline, "deadline has expired"
30    assert not self.finalized
31
32    self.funders[msg.sender] += msg.value
33
34# Enough money was raised! Send funds to the beneficiary
35@external
36def finalize():
37    assert block.timestamp >= self.deadline, "deadline has not expired yet"
38    assert self.balance >= self.goal, "goal has not been reached"
39    assert self.balance > 0
40    self.finalized = True
41
42    send(self.beneficiary, self.balance)
43
44# Let participants withdraw their fund
45@external
46def refund():
47    assert block.timestamp >= self.deadline and self.balance < self.goal
48    assert not self.finalized
49    assert self.funders[msg.sender] > 0
50
51    value: uint256 = self.funders[msg.sender]
52    self.funders[msg.sender] = 0
53
54    send(msg.sender, value)

Most of this code should be relatively straightforward after going through our previous examples. Let’s dive right in.

 9funders: HashMap[address, uint256]
10beneficiary: address
11deadline: public(uint256)
12goal: public(uint256)
13timelimit: public(uint256)
14finalized: bool

Like other examples, we begin by initiating our variables. Some variables like deadline, goal and timelimit are declared with the public function, making them readable by external callers. Variables without public are, by default, private.

Note

Unlike the existence of the function public(), there is no equivalent private() function. Variables simply default to private if initiated without the public() function.

The funders variable is initiated as a mapping where the key is an address, and the value is a number representing the contribution of each participant. The beneficiary will be the final receiver of the funds once the crowdfunding period is over—as determined by the deadline and timelimit variables. The goal variable is the target total contribution of all participants.

16# Setup global variables
17@deploy
18def __init__(_beneficiary: address, _goal: uint256, _timelimit: uint256):
19    self.beneficiary = _beneficiary
20    self.deadline = block.timestamp + _timelimit
21    self.timelimit = _timelimit
22    assert _goal > 0, "Goal must be non-zero"
23    self.goal = _goal

Our constructor function takes 3 arguments: the beneficiary’s address, the goal in wei value, and the difference in time from start to finish of the crowdfunding. We initialize the arguments as contract variables with their corresponding names. Additionally, a self.deadline is initialized to set a definitive end time for the crowdfunding period.

Now let’s take a look at how a person can participate in the crowdfund.

25# Participate in this crowdfunding campaign
26@external
27@payable
28def participate():
29    assert block.timestamp < self.deadline, "deadline has expired"
30    assert not self.finalized
31
32    self.funders[msg.sender] += msg.value

Once again, we see the @payable decorator on a method, which allows a person to send some ether along with a call to the method. In this case, the participate() method accesses the sender’s address with msg.sender and the corresponding amount sent with msg.value. The contribution is added to the funders HashMap, which maps each participant’s address to their total contribution amount.

34# Enough money was raised! Send funds to the beneficiary
35@external
36def finalize():
37    assert block.timestamp >= self.deadline, "deadline has not expired yet"
38    assert self.balance >= self.goal, "goal has not been reached"
39    assert self.balance > 0
40    self.finalized = True
41
42    send(self.beneficiary, self.balance)

The finalize() method is used to complete the crowdfunding process. However, to complete the crowdfunding, the method first checks to see if the crowdfunding period is over and that the balance has reached/passed its set goal. If those two conditions pass, the contract sends the collected funds to the beneficiary.

Note

Notice that we have access to the total amount sent to the contract by calling self.balance, a variable we never explicitly set. Similar to msg and block, self.balance is a built-in variable that’s available in all Vyper contracts.

We can finalize the campaign if all goes well, but what happens if the crowdfunding campaign isn’t successful? We’re going to need a way to refund all the participants.

44# Let participants withdraw their fund
45@external
46def refund():
47    assert block.timestamp >= self.deadline and self.balance < self.goal
48    assert not self.finalized
49    assert self.funders[msg.sender] > 0
50
51    value: uint256 = self.funders[msg.sender]
52    self.funders[msg.sender] = 0
53
54    send(msg.sender, value)

In the refund() method, we first check that the crowdfunding period is indeed over and that the total collected balance is less than the goal with the assert statement . If those two conditions pass, we let users get their funds back using the withdraw pattern.