B9lab Logo
Tezos Developer Portal
Developer PortalDeveloper Portal

SmartPy Exercise - Let's Code Some More

A smart contract to crowdfund

Now that we tested a more complicated storage structure, let's dive into another example.

Does crowdfunding sound familiar to you?

The topic is very interesting because there are many forms in which to conduct crowdfunding. The basic idea is to finance a project or enterprise through the contributions of many, i.e. "the crowd", instead of convincing a closed group of traditional financing institutions (i.e. banks, venture capital funds, investment firms, etc.) to provide capital for a project.

We want to explore the possibilities of a decentralised crowdfunding approach.

For this, a public blockchain like Tezos is well-fitted. The openness of a public blockchain platform should be suitable as crowdfunding is in its nature an open, transparent, and public form of financing. The main idea is that a project initiator, i.e. the founder, is able to kick off a crowdfund with just a few clicks.

The crowdfunding smart contract

Each project will be represented by an individual Tezos smart contract. The smart contract has to fulfil three functions:

  • send_fund: Every user with a Tezos address can use this function to participate in the crowdfund. For reasons of simplicity, each address can only make one payment;
  • pay_off: The project initiator can transfer the collected funds to a different address. This can only be done if the funding target is met. For reasons of simplicity, the owner can call this function only after the deadline has passed;
  • refund: In case the set funding goal is not met in a specified time frame, each participant can request a refund.

Moreover, the storage has to contain the following information:

  • minAmount: This is the minimum limit to be achieved for a crowdfund to be considered successful, and
  • maxTime: This value represents the deadline by which the minAmount has to be met.

Test-driven approach

Surely, you already have some ideas for the smart contract, and maybe you also started seeing possible difficulties and stumbling blocks.

So, we start with the test ensuring a functioning smart contracts fulfilling the requirements as specified before.

@sp.add_test(name = "successful Crowdfunding")
def successful():

    # dummy addresses
    owner= sp.address("tz1xowner")
    user1= sp.address("tz1xuser1")
    user2= sp.address("tz1xuser2")
    user3= sp.address("tz1xuser3")

    # minAmonut= 20
    # deadline= now + 3d
    contract = Crowdfunding(owner, 
    sp.tez(20), sp.timestamp(int(time.time())+259200))
    scenario = sp.test_scenario()
    scenario+= contract

    scenario+= contract.send_fund().run(sender=user1, 
    amount=sp.tez(10), now = sp.timestamp(int(time.time())+100))

    scenario+= contract.send_fund().run(sender=user2, 
    amount=sp.tez(8), now = sp.timestamp(int(time.time())+200))

    # will fail because user1 already sent
    scenario += contract.send_fund().run(sender=user1, 
    amount=sp.tez(12), now = sp.timestamp(int(time.time())+300), valid = False)

    # will fail because 2 tez missing
    scenario += contract.pay_off().run(sender=owner, 
    now = sp.timestamp(int(time.time())+300), valid = False)

    scenario += contract.send_fund().run(sender=user3, 
    amount=sp.tez(15), now = sp.timestamp(int(time.time())+400))

    # will fail because deadline not reached
    scenario += contract.pay_off().run(sender=owner, 
    now = sp.timestamp(int(time.time())+500), valid = False)

    scenario += contract.pay_off().run(sender=owner, 
    now = sp.timestamp(int(time.time())+259200))

Try to implement a contract, which can pass the tests.

As you can see, you will need to implement a class called Crowdfunding:

  • The storage will need to keep track on the fundings;
  • The contract will have an owner, who can call the pay_off function;
  • Several conditions must be met for pay_off and refund.

There are a few keywords that you will need but we have not seen so far:

  • sp.amount will give you the amount of mutez(in microtez) in the transaction,
  • You can access the balance of the contract with sp.balance,
  • sp.now will give you the timestamp of the block with the transaction.

At the end, such a contract will look like:

import time
import smartpy as sp

class Crowdfunding(sp.Contract):
    def __init__(self, owner, minAmount, maxTime):
        self.init(funding=sp.map(tkey=sp.TAddress, tvalue=None), owner=owner, minAmount=minAmount, maxTime=maxTime)

    def send_fund(self, params):
        sp.verify(self.data.maxTime >= sp.now) 
        self.data.funding[sp.sender]= sp.amount

    def pay_off(self, params):
        sp.verify(self.data.minAmount <= sp.balance)
        sp.verify(self.data.maxTime <= sp.now)
        sp.send(self.data.owner, sp.balance)

    def refund(self, params):
        sp.verify(self.data.maxTime < sp.now)
        sp.verify(self.data.minAmount > sp.balance)
        sp.send(sp.sender, self.data.funding[sp.sender])
        del self.data.funding[sp.sender]

The smart contract allows:

  • A user with a Tezos address to send funds - Remember, a payment can only be made once;
  • To transfer the collected funds to a different address of the initiator once the funding target is met;
  • The crowdfunding participants to request a refund, if the funding target isn't met;
  • For the storage to contain the minAmount and maxTime.
Discuss on Slack