Crowdfunding Smart Contract on Stacks using 
                                                     Clarity

Crowdfunding Smart Contract on Stacks using Clarity

In this tutorial, I will be walking you through developing a crowdfunding clarity smart contract along with a brief introduction of the Stacks Blockchain and clarity programing language. In this tutorial we will be writing code, which is being executed on the blockchain, meaning that We might go a little deep into the mechanism of how clarity works and how can we get the best of it to perform our tasks.

So, let's start by shedding some light on the Stacks Blockchain.

Screenshot (2120).png

Stacks Blockchain :

Stacks is a Layer-1 Blockchain, and it allows the developers to Build Defi, Nfts, Apps and smart contracts integrated with security of the Bitcoin. The native crypto of the Stacks Blockchain is Stx.

Stacks uses a relatively new consensus mechanism known as the "Proof of Transfer (pox)". Through Proof of transfer, we secure a relatively new Blockchain (Stacks) with an already secure established Blockchain (Bitcoin). Miners will have to transfer some bitcoin to some Stx holders to show their proof of transfer and that they deserve to mine new blocks.

Want to learn more about how to build on Stacks Blockchain? Visit Here

Clarity Language :

So, now that we know basic overview of Stacks, the next question is how we can write smart contracts on it. Clarity, the Programing Language is the answer to this question.

Screenshot (2121).png

Clarity is an interpreted and not compiled language like most other smart contract languages. This means that the code you write after debugging will be pushed to the chain exactly as it is (Other languages send the bytecode i.e., unreadable version of the code on chain). The reason why clarity does this, is to ensure transparency and that the users know exactly what they are doing when they interact with the smart contract.

Other properties of clarity include its built-in support against reentrancy (You can learn more about it on the link below) and guards against overflow and underflow. Now these are vulnerabilities in which millions of dollars' worth crypto of users have been hacked, so clarity stands out by disallowing them on the language level. You can learn more about reentrancy Here

Want to master Clarity Programing Language Visit Here

So, let's waste no time, and start right away beginning with what exactly is crowdfunding.

cr.jfif

Crowdfunding is the process by which we can fund a project or campaign by raising capital from the people. Since we are building a crowdfunding smart contract using clarity, the investors will be able to send the STX coin to the contract.

So, before we start to code, It will be super productive if we brainstorm our clarity smart contract and list all the functionalities, we want from it.

  1. We would want the users to create a campaign or a project for crowdfunding.
  2. The users from all over the internet will be able to invest their stx into the project.
  3. If the project crowdfunding, went successful we would allow the creator of the project to withdraw stx from the contract.
  4. If the project could not raise enough funds in the due time, we would allow the investors of the project to get their refund from the contract.

Alright, now that we have mapped the functionality which we want from our project we can start the most exciting part of the tutorial i.e. writing clarity code which would be deployed to the Stacks Blockchain

To write and deploy clarity code on your computer you would need to install clarinet and have a Hiro wallet installed. Since, most of you would have already installed them I would simply skip them to avoid making this tutorial long but I would be leaving a link for their installation walkthrough. [Installation guide for clarinet and Hiro wallet] (cuddleofdeath.hashnode.dev/guide-to-deployi..)

Now, let's start finally start writing code. It is generally a code Clarity Practice to start the code by listing all the errors which we will be throwing from the code. Declaring them as constant will increase the code readability and also help the users of this contract to help them identify what mistakes they are making while executing.

(define-constant error-general (err u1))
(define-constant error-not-owner (err u2))
(define-constant error-campaign-does-not-exist (err u3))
(define-constant error-campaign-inactive (err u4))
(define-constant error-invest-stx-transfer-failed (err u5))
(define-constant error-no-investment (err u6))
(define-constant error-campaign-already-funded (err u7))
(define-constant error-refund-stx-transfer-failed (err u8))
(define-constant error-target-not-reached (err u9))
(define-constant error-funding-stx-transfer-failed (err u10))
(define-constant error-already-funded (err u11))

All the errors are defined in clear and concise variables so that they are self-explanatory however let me give a brief overview of them.

  1. The error-general will be thrown, when the user did not provide correct data for creating a campaign.
  2. The error-not-owner will be thrown, when some other person except the campaign owner tries to withdraw amount from campaign.
  3. The error-campaign-does-not-exist will arise when an invalid campaign id is provided.
  4. The error-campaign-inactive will arise when an investor will try to send stx to inactive campaign.
  5. The error-invest-stx-transfer-failed will be thrown when the stx transfer failed from the investor while calling the invest function.
  6. When the campaign owner tries to withdraw from a campaign with no investment, error-no-investment will be thrown.
  7. When an investor will try to invest in a crowdfund where he has already invested.
  8. When an investor will try to refund from a crowdfund, but the campaign owner has already withdrawn from the funds (error-refund-stx-transfer-failed)
  9. The error-target-not-reached will be executed when the target of the refund has not been reached but the owner tries to withdraw
  10. The error-funding-stx-transfer-failed when the transfer of the Stx failed from the funds to the campaign owner address.
  11. When a campaign owner will try to withdraw again from a campaign, error-campaign-already-funded will be thrown.

Now, let us define some state variables which we will be updating throughout the execution of this code.

(define-data-var campaign-id-nonce uint u0)
(define-data-var total-campaigns-funded uint u0)
(define-data-var total-investments uint u0)
(define-data-var total-investment-value uint u0)

We have initiated all of these variables with 0, "campaign-id-nonce" will serve as the total number of projects created while the "total-campaigns-funded" will mean the number of projects which have been successfully completed by the users. We will increment the value of total-campaigns-funded when an owner will withdraw funds from a crowdfund. The variable total-investments will show the number of investments which have been done from this contract, while the variable total-investment-value is the amount of value in stx which have been sent to the contract.

Now, we will be creating some mapping from the key "campaign id" to the values of campaign name, link, description, creator, goal, time limit, current status and total investors.

(define-map campaigns ((campaign-id uint))
    (
        (name (buff 64))    
        (fundraiser principal)        
        (goal uint)                    
        (target-block-height uint)
    ))


(define-map campaign-information ((campaign-id uint))
    (
        (description (buff 280))    
        (link (buff 150))        
    ))

In the first mapping of campaigns, we have mapped the campaign id to the name, address of the fundraiser, goal of the fundraiser (total amount required) and target-block-height (the time before which we need the crowdfunding to be completed). In the 2nd mapping, campaign-information we have mapped campaign id to the description and link of the crowdfunding.

(define-map campaign-totals ((campaign-id uint))
    (
        (total-investment uint)
        (total-investors uint)
    ))


(define-map campaign-status ((campaign-id uint))
    (
        (target-reached bool)
        (target-reached-height uint)
        (funded bool)    
    ))

In the third mapping, campaign-totals, we mapped the campaign id to the total investment and the total investors. We will be updating the values of these variable every time a refund or investment is made to a specific campaign. The fourth mapping stores the status of the campaign, target-reached means if the campaign got its funding target within the time duration, target-reached-height stores the block value at which the funding reached its target, and the funded bool means if the owner of the crowdfunding has withdrawn Stx from the contract.

(define-map investments-principal ((campaign-id uint) (investor principal))
    (
        (amount uint)
    ))

The last mapping is a little interesting one as it is quite different from the ones we explained before. It uses a composite key of campaign id and investor principal mapped to the total amount which is invested by the investor. This will be helpful when we will implement the refund function.

Now we will be creating some helper functions, which will be read only meaning that they will not cost any gas while executing, and we will be using these functions later in our code. For example, look at the first helper function "get-campaign-id-nonce", all it does is that it returns variable "campaign-id-nonce". All the following helper functions are basically the same except that they return different variables.

(define-read-only (get-campaign-id-nonce)
    (ok (var-get campaign-id-nonce))
    )


(define-read-only (get-total-campaigns-funded)
    (ok (var-get total-campaigns-funded))
    )


(define-read-only (get-total-investments)
    (ok (var-get total-investments))
    )


(define-read-only (get-total-investment-value)
    (ok (var-get total-investment-value))
    )


(define-read-only (get-campaign (campaign-id uint))
    (ok (map-get? campaigns ((campaign-id campaign-id))))
    )


(define-read-only (get-campaign-information (campaign-id uint))
    (ok (map-get? campaign-information ((campaign-id campaign-id))))
    )

(define-read-only (get-campaign-totals (campaign-id uint))
    (ok (map-get? campaign-totals ((campaign-id campaign-id))))
    )


(define-read-only (get-campaign-status (campaign-id uint))
    (ok (map-get? campaign-status ((campaign-id campaign-id))))
    )

Now that we have made a basic structure for our project, we can implement the main function of this smart contract.

  1. Create Campaign
  2. Invest in Campaign
  3. Refund
  4. Withdraw

We will start by writing the code of first function Create Campaign. This function will take the following parameters.

  • Name of campaign
  • Description of campaign
  • Url of campaign
  • goal (total amount required)
  • Duration (Time before which we require it to be completed) The first thing we do, in this function is to create a campaign-id variable initializing it with the incremented value of the campaign-id-nonce.
(define-public (create-campaign (name (buff 64)) (description (buff 280)) 
(link (buff 150)) (goal uint) (duration uint))
    (let ((campaign-id (+ (var-get campaign-id-nonce) u1)))

Now we perform state changes in all 4 mappings related to the campaign. All these campaigns mappings have already been explained in the previous parts and we are updating the mappings using "map-set" to the values we have received in the function parameters.

        (if (and
                (map-set campaigns ((campaign-id campaign-id))
                    (
                        (name name)
                        (fundraiser tx-sender)
                        (goal goal)
                        (target-block-height (+ duration block-height))
                    ))
  (map-set campaign-information ((campaign-id campaign-id))
                    (
                        (description description)
                        (link link)
                    ))

Notice, how we updated the value of fundraiser in the campaigns mapping by the "tx-sender", this just means that whoever called the function "create-campaign" will be the fundraiser.

                (map-set campaign-totals ((campaign-id campaign-id))
                    (
                        (total-investment u0)
                        (total-investors u0)
                    ))
                (map-set campaign-status ((campaign-id campaign-id))
                    (
                        (target-reached false)
                        (target-reached-height u0)
                        (funded false)
                    ))
                )

We assigned the total-investments and the total-investors by zero, as at the time of creation of campaign it had no investments. The target-reached and the funded bools in the campaign-status mapping will be false initially.

            (begin
                (var-set campaign-id-nonce campaign-id)
                (ok campaign-id))
            error-general ;; else
            )
        )
    )

We update the value of the campaign-id-nonce and throw an "error general", if we fail to update the value of mappings.

Now, we move towards writing the invest function which happens to be a relatively harder function to implement. The idea behind function is to send stx into a campaign which was created by a principal. This function will take two parameters that are the id of the campaign which a user wants to fund and how much Stx are they willing to send(amount). The first thing we do, is to check whether the campaign id provided by the user is correct otherwise we throw a error-campaign-does-not-exist.

(define-public (invest (campaign-id uint) (amount uint))
    (let (
        (campaign (unwrap! (map-get? campaigns ((campaign-id campaign-id))) 
error-campaign-does-not-exist))

We now, initialize two more variables called the status and total(totat stx which the campaign has received)

        (status (unwrap-panic (map-get? campaign-status 
    ((campaign-id campaign-id)))))
        (total (unwrap-panic (map-get? campaign-totals 
      ((campaign-id campaign-id)))))
        )

We now, check whether the campaign is still indeed active. The way we can check this if to see if the block height provided by the campaign owner has not passed and the "target reached" bool from the status mapping is false. We have enclosed our expression inside a "and operator", and we will only move forward if the whole expression returns true. If the check passes, we transfer the Stx from the investor to the contract using the "stx-transfer" built in function If you are unfamiliar with "asserts!", "unwrap!", you can go through these official docs for clarity.

Docs for asserts!

Docs for unwrap!

Docs for stx-transfer

        (asserts! (and (< block-height (get target-block-height campaign)) 
        (not (get target-reached status))) error-campaign-inactive)
        (unwrap! (stx-transfer? amount tx-sender 
        (as-contract tx-sender)) error-invest-stx-transfer-failed)

Now, as the stx transfer went successful we will increment the values of campaign-totals mapping by the amount which the investor has sent. We first initialize a local variable with previous total and amount sent by the user and proceed by updating the mappings.

        (let (
            (new-campaign-total (+ (get total-investment total) amount))
            )
            (if (and
                    (map-set campaign-totals ((campaign-id campaign-id))
                        (
                            (total-investment new-campaign-total)
                            (var-set total-investors  (+ (var-get total-investors) u1))

                        ))

We now increment the total-investments and the total-investment-value by 1 and the amount sent by the user respectively. We also perform the check that if the campaign has achieved its target amount, then we update the campaign-status mapping and change its target-reached bool to true and set the current block height value to the "target-reached-height". And also, don't let us forget to increment the total-campaigns-funded by 1.


                (begin
                    (var-set total-investments 
      (+ (var-get total-investments) u1))
                    (var-set total-investment-value 
      (+ (var-get total-investment-value) amount))
                    (if (>= new-campaign-total (get goal campaign))
                        (begin
                            (map-set campaign-status ((campaign-id campaign-id))
                                (
                                    (target-reached true)
                                    (target-reached-height block-height)
                                    (funded false)
                                ))
                            (var-set total-campaigns-funded 
(+ (var-get total-campaigns-funded) u1))
                            (ok u2) ;; funded and target reached
                            )
                        (ok u1) ;; else: funded but target not yet reached
                        )
                    )
                error-general ;; else
                )
            )
        )
    )

As expected, if the function could not succeed executing then we would throw the "error-general" which we defined at the top of the contract.

Now, let's implement the refund function which would allow the investors of the campaign to withdraw their funds from the campaign if the campaign remained unsuccessful meaning that the time passed, and enough funds were not collected. This function will take just one parameter i.e., campaign-id from which you want to refund your amount. The first thing we do, is to check whether the campaign id provided by the user is correct otherwise we throw an "error-campaign-does-not-exist".

(define-public (refund (campaign-id uint))
    (let (
        (campaign (unwrap! (map-get? campaigns ((campaign-id campaign-id))) 
        error-campaign-does-not-exist))

We now, initialize two more variables called the status and total(totat stx which the campaign has received)


        (status (unwrap-panic (map-get? campaign-status ((campaign-id campaign-id)))))
        (total (unwrap-panic (map-get? campaign-totals ((campaign-id campaign-id)))))

We now, get the prior investments of the user on this specific campaign using the "investments-principal" mapping using the campaign id and the principal of the tx sender. We proceed by writing some asserts, first of which is to check that the campaign is not already funded,

        (prior-investment (default-to u0 (get amount (map-get? investments-principal 
        ((campaign-id campaign-id) (investor tx-sender))))))    
        )

        (asserts! (not (get target-reached status)) error-campaign-already-funded)

We also check that the caller of the function has invested some amount in this campaign. If this assert succeeds, we will again use the stx-transfer, but this time we will transfer the stx from the contract to the principal of the investor.

        (asserts! (> prior-investment u0) error-no-investment)
        (unwrap! (as-contract (stx-transfer? prior-investment tx-sender)) 
        error-refund-stx-transfer-failed)

Now, as the stx transfer went successful we will decrement the values of campaign-totals mapping by the amount which the investor has been refunded. We first initialize a local variable "new-campaign-total" by using the difference between previous total and prior-investment of the user and proceed by updating the mapping of "campaign-totals".

        (let (
            (new-campaign-total (- (get total-investment total) prior-investment))
            )
            (if (and
                    (map-set campaign-totals ((campaign-id campaign-id))
                        (
                            (total-investment new-campaign-total)
                            (total-investors (- (get total-investors total) u1))
                        ))

                    )

We now decrement the total-investments and the total-investment-value by 1 and the amount refunded by the user respectively.

                (begin
                    (var-set total-investments (- (var-get total-investments) u1))
                    (var-set total-investment-value (- (var-get total-investment-value) 
                    prior-investment))
                    (ok u1)
                    )
                error-general ;; else
                )
            )
        )
    )

Now, all that remains is the collect function which will be called by the creator of the campaign, and it will transfer the funds that campaign received to the wallet of the creator of the campaign. This function will take just one parameter i.e. campaign-id from which the campaign owner wants to withdraw the funds. The first thing we do, is to check whether the campaign id provided by the user is correct otherwise we throw an error-campaign-does-not-exist.

(define-public (collect (campaign-id uint))
    (let (
        (campaign (unwrap! (map-get? campaigns ((campaign-id campaign-id))) error-campaign-does-not-exist))

We now, initialize two more variables called the status and total(totat stx which the campaign has received)


        (status (unwrap-panic (map-get? campaign-status ((campaign-id campaign-id)))))
        (total (unwrap-panic (map-get? campaign-totals ((campaign-id campaign-id)))))
        )

We will now need to write some more asserts, as this is a relatively sensitive function, and we will need to make sure that only the right person should be able to call this only at the right time. The first check will be to check that the caller of this function is the owner of the campaign. The 2nd assert will be to check that the owner has not previously withdrawn the funds from the contract.

        (asserts! (is-eq (get fundraiser campaign) tx-sender) error-not-owner)
        (asserts! (not (get funded status)) error-already-funded)

We will proceed by writing one more assert, it will check that the campaign has achieved its target otherwise we will throw a "error-target-not-reached". If all the asserts have passed, we have made sure that indeed the caller of the function is the campaign owner, and the campaign was successful. We will now again use the stx-transfer to transfer the stx from the contract to the principal of the campaign owner.

        (asserts! (get target-reached status) error-target-not-reached)
        (unwrap! (as-contract (stx-transfer? (get total-investment total) tx-sender )) error-funding-stx-transfer-failed)

We will now, change the bool "funded" of the campaign status mapping as the owner has withdrawn from the contract.

        (asserts! (map-set campaign-status ((campaign-id campaign-id))
            (
                (target-reached true)
                (target-reached-height (get target-reached-height status))
                (funded true)
            )) error-general)
        (ok u1)
        )
    )

As expected, if the function could not succeed executing then we would throw the "error-general" which we defined at the top of the contract.

You deserve a pat on your back as you have completed this tutorial and have created a crowdfunding contract on Stacks blockchain. This is definitely start of something special and you should now be more confident in writing clarity code.

You can get the complete code from this Repository

If there is any line of code, that you are having a hard time understanding, do let me know in the comments, I will be more than happy to help.

thats.jfif