Creating A decentralized Crowdfund Contract on SUI

Hey, diving right in as the title explains it all, but first things first you need to have an understanding of Sui move a and also having your sui move client installed in your laptop

to go right into the code, you can visit the github repo : SUI CROWDFUND

You are already leaving!, A simple thank you will do?

The Smart Contract

This section we will majorly look at the move code.

starting with the move code

sui move new crowdfund

this will create a crowdfund directory within the file path you ran the command on your terminal.

you should have a directory named crowdfund with the file structure as seen below

If this is what you have, then you’re on the right track, now open the directory on vs code, navigate to your sources directory and open crowdfund.move file.

starting off with the needed imports and error(s):

module crowdfund::crowdfund;

use std::string::String;
use sui::clock::{Clock};
use sui::coin::{Self, Coin};
use sui::sui::SUI;
use sui::balance::{Self, Balance};

// Error constants
const EINVALID: u64 = 0;

The first import is a data type for accommodating String like data.

The second import is an is a built in object provided by sui that can be used to get the current timestamp and Epoch of a published package

The third import is also another built in object that is use to accommodate tokens whether existing or created Tokens

the forth import is another built object that specifies the sui token of the Sui ecosystem

the fifth import is used to get the balance of token in a object or an account

Moving into the structs

module crowdfund::crowdfund;

use std::string::String;
use sui::clock::{Clock};
use sui::coin::{Self, Coin};
use sui::sui::SUI;
use sui::balance::{Self, Balance};

// Error constants
const INVALID: u64 = 0;

// Struct to track contributors
public struct Contributor has store, drop, copy {
        wallet_address: address,
        amount: u64,
        timestamp: u64,
}
 // Struct to manage all crowdfunds
public struct CrowdfundManager has key {
        id: UID,
        crowdfunds: vector<address>, // Store all crowdfund addresses
}

// Main crowdfund struct (You can switch it up to what you want)
public struct Crowdfund has key {
        id: UID,
        name: String,
        description: String,
        creator: address,
        crowdfund_type: u8, // 0 for grant, 1 for ICO
        target_amount: u64,
        current_amount: u64,
        deadline: u64,
        contributors: vector<Contributor>,
        funds: Balance<SUI>,
        is_active: bool,
        is_successful: bool
}
  • The Contributor struct, just as the name implies contains details of accounts that contributes to a running crowdfund

  • The CrowdfundManager struct is like a table that stores all created Crowdfund

  • The Crowdfund struct contains the details of a Crowdfund

See how simple it is? So mindful, So demure :)

Next, we add the initialization function

    // Initialize the crowdfunding platform
    fun init(ctx: &mut TxContext) {
        let manager = CrowdfundManager {
            id: object::new(ctx),
            crowdfunds: vector::empty<address>(),
        };
        transfer::share_object(manager)
    }

Basically, we are initialing the CrowdfundManger’s crowdfunds vector to empty to enable us add created crowdfund to the CrowdfundManager. after which The CrowdfundManager object is publicly shared (So anyone can have access to the CrowdfundManager

Moving into the different function

Firstly, we will be looking into Create Crowdfund function

public fun create_crowdfund(
    manager: &mut CrowdfundManager,
    name: String,
    description: String,
    crowdfund_type: u8,
    target_amount: u64,
    deadline: u64,
    ctx: &mut TxContext,
) {
    let sender = tx_context::sender(ctx);

    let crowdfund_property = Crowdfund {
        id: object::new(ctx),
        name,
        description,
        creator: sender,
        crowdfund_type,
        target_amount,
        current_amount: 0,
        deadline,
        contributors: vector::empty<Contributor>(),
        funds: balance::zero(),
        is_active: true,
        is_successful: false,
    };

    let property_id = object::id_address(&crowdfund_property);
    vector::push_back(&mut manager.crowdfunds, property_id);

    transfer::share_object(crowdfund_property);
}

Breakdown of what’s going on… This function basically create a crowdfund for contributors to contribute to… notice most of the argument being passed in… I will explain key things here:

  1. manager: This is the previously shared CrowfundManger Object that anyone on the sui blockchain can have access too. One key thing to notice is that it is a mutable reference because we are going to update the vector structure in the CrowfundManger object when we append the newly created Crowdfund.

  2. sender: this is gotten from the transaction context… so when the function is called. the transaction context has certain properties of that particular function call/transaction and that includes the sender or caller and the sender becomes the creator/owner of the soon to be created Crowdfund

  3. contributors: this property found within the object instance is a vector that is initialized to an empty state(i.e when you create an empty array) this give room for us keep records of the people who contributed to the crowdfund

  4. current_amount: this shows the current amount being contributed and on the first instance of creating the crowdfund… this is set to 0 and it can increase as contributor continue to contribute to the crowdfund

  5. deadline: This is basically time in timestamps which shows how long the crowdfund campaign can last.. there are numerous ways of doing this but let’s keep it this way

  6. funds: the stores actual contributions made with the coinType in this case sui

now after the crowdfund(crowdfund_property) is created, I had to get the crowdfund id via an immutable reference to append or register the created crowdfund in the CrowdfundManager vector property

 let property_id = object::id_address(&crowdfund_property);
        vector::push_back(&mut manager.properties, property_id);

        transfer::share_object(crowdfund_property);

after which the newly created crowdfund is being shared, so anyone can have access to the crowdfund

Now, we will look into the Contribute Function

public fun contribute(crowdfund: &mut Crowdfund, payment: Coin<SUI>, clock: &Clock, ctx: &mut TxContext ){

        let sender = tx_context::sender(ctx);
        let amount = coin::value(&payment);

        assert!(crowdfund.is_active, EINVALID);
        let contributor = Contributor {
            wallet_address: sender,
            amount,
            timestamp: clock.timestamp_ms()
        };

        vector::push_back(&mut crowdfund.contributors, contributor);



        let payment_balance = coin::into_balance(payment);
        crowdfund.current_amount = crowdfund.current_amount + amount;
        balance::join(&mut crowdfund.funds, payment_balance);
 }

looking at the arguments, we have the crowdfund, payment, clock, and transaction context…

The crowdfund is the particular crowdfund a contributor wants to contribute to hence the use of a mutable reference(&mut) as they will changes made in the crowdfund object.

The payment, the actual sui token being contributed to the crowdfund.

The clock, is a sui framework for anything that deals with date and time.

Breakdown of what is going on in the function:

Firstly, we get the sender from the transaction context,

then, we get amount of sui being contributed from the sui framework, coin via the value function(coin::value(…))

then, we check if the crowdfund campaign is still active via assert!(…);… only when it is true that a contributor can be able to make contributions

if the crowdfund is still active, we create a contributor object that contains the wallet_address(the contributor address), amount(amount of sui being contributed), and timestamp(the time at which the contribution was made)

then, we append or register the contributor into the crowdfund.contributors vectors.

once that is done, we fetch the actual payment_balance from payment argument via the sui framework, coin (coin::into_balance());

then we update the current_amount property of the crowdfund as well the funds balance property in the crowdfund

Thirdly, we will look into the Withdraw Function (Performed by the crowdfund creator)

public fun withdraw (crowdfund: &mut Crowdfund, clock: &Clock, ctx: &mut TxContext) {
      assert!(clock.timestamp_ms() > crowdfund.deadline, EINVALID);
      assert!(crowdfund.is_active, EINVALID);

      let available_funds = balance::value(&crowdfund.funds);

      if (available_funds > 0 ) {
          let withdraw_coin = coin::from_balance(balance::split(&mut crowdfund.funds, available_funds), ctx);

           crowdfund.is_active = false;            

          transfer::public_transfer(withdraw_coin, crowdfund.creator)
      }

}

firstly we check if the current timestamp has passed the crowdfund deadline, then we check if the crowdfund is still active.

if those two checks successfully, we check the available_funds in crowdfund via balance::value(&crowdfund.funds);

now if available_funds is greater than 0… we first split the balance (essentially subtracting available_funds from crowdfund.funds) which results in the withdraw_coin

Keep in mind that literally everything on sui is an object… the coin you transact with all object, hence the withdraw_coin

Then we set the active state of the crowdfund to false

then transfer the withdraw_coin to crowdfund creator

Finally, A getter function to fetch details about a crowdfund

      public fun get_crowdfund_info(crowdfund: &Crowdfund): (
        String,    // name
        String,    // description
        address,   // creator
        u8,        //Crowdfund Type
        u64,      // target amount
        u64,      // current amount
        u64,      // deadline
        bool,     // is active
        bool      // is successful
    ) {
        (
            crowdfund.name,
            crowdfund.description,
            crowdfund.creator,
            crowdfund.crowdfund_type,
            crowdfund.target_amount,
            crowdfund.current_amount,
            crowdfund.deadline,
            crowdfund.is_active,
            crowdfund.is_successful,
        )
    }

This function fetches the main properties of a particular crowdfund…

HERE IS A FULL CODE SNIPPET

module crowdfund::crowdfund;

use std::string::String;
use sui::balance::{Self, Balance};
use sui::clock::Clock;
use sui::coin::{Self, Coin};
use sui::sui::SUI;

// Error constants
const EINVALID: u64 = 0;

// Struct to track contributors
public struct Contributor has copy, drop, store {
    wallet_address: address,
    amount: u64,
    timestamp: u64,
}
// Struct to manage all crowdfunds
public struct CrowdfundManager has key {
    id: UID,
    crowdfunds: vector<address>, // Store all crowdfund addresses
}

// Main crowdfund struct (You can switch it up to what you want)
public struct Crowdfund has key {
    id: UID,
    name: String,
    description: String,
    creator: address,
    crowdfund_type: u8, // 0 for grant, 1 for ICO
    target_amount: u64,
    current_amount: u64,
    deadline: u64,
    contributors: vector<Contributor>,
    funds: Balance<SUI>,
    is_active: bool,
    is_successful: bool,
}

// Initialize the crowdfunding platform
fun init(ctx: &mut TxContext) {
    let manager = CrowdfundManager {
        id: object::new(ctx),
        crowdfunds: vector::empty<address>(),
    };
    transfer::share_object(manager)
}

public fun create_crowdfund(
    manager: &mut CrowdfundManager,
    name: String,
    description: String,
    crowdfund_type: u8,
    target_amount: u64,
    deadline: u64,
    ctx: &mut TxContext,
) {
    let sender = tx_context::sender(ctx);

    let crowdfund_property = Crowdfund {
        id: object::new(ctx),
        name,
        description,
        creator: sender,
        crowdfund_type,
        target_amount,
        current_amount: 0,
        deadline,
        contributors: vector::empty<Contributor>(),
        funds: balance::zero(),
        is_active: true,
        is_successful: false,
    };

    let property_id = object::id_address(&crowdfund_property);
    vector::push_back(&mut manager.crowdfunds, property_id);

    transfer::share_object(crowdfund_property);
}

public fun contribute(
    crowdfund: &mut Crowdfund,
    payment: Coin<SUI>,
    clock: &Clock,
    ctx: &mut TxContext,
) {
    let sender = tx_context::sender(ctx);
    let amount = coin::value(&payment);

    assert!(crowdfund.is_active, EINVALID);
    let contributor = Contributor {
        wallet_address: sender,
        amount,
        timestamp: clock.timestamp_ms(),
    };

    vector::push_back(&mut crowdfund.contributors, contributor);

    let payment_balance = coin::into_balance(payment);
    crowdfund.current_amount = crowdfund.current_amount + amount;
    balance::join(&mut crowdfund.funds, payment_balance);
}

public fun withdraw(crowdfund: &mut Crowdfund, clock: &Clock, ctx: &mut TxContext) {
    assert!(clock.timestamp_ms() > crowdfund.deadline, EINVALID);
    assert!(crowdfund.is_active, EINVALID);

    let available_funds = balance::value(&crowdfund.funds);

    if (available_funds > 0) {
        let withdraw_coin = coin::from_balance(
            balance::split(&mut crowdfund.funds, available_funds),
            ctx,
        );

        crowdfund.is_active = false;

        transfer::public_transfer(withdraw_coin, crowdfund.creator)
    }
}

public fun get_crowdfund_info(
    crowdfund: &Crowdfund,
): (
    String, // name
    String, // description
    address, // creator
    u8, //Crowdfund Type
    u64, // target amount
    u64, // current amount
    u64, // deadline
    bool, // is active
    bool, // is successful
) {
    (
        crowdfund.name,
        crowdfund.description,
        crowdfund.creator,
        crowdfund.crowdfund_type,
        crowdfund.target_amount,
        crowdfund.current_amount,
        crowdfund.deadline,
        crowdfund.is_active,
        crowdfund.is_successful,
    )
}

If you got to this, I want to say that you rock 🤟🔥

Now with this, you can run tests and publish the smart contract… on the next drop… I will show you how to write tests on sui move

NOW SAY THANK YOU!

You’re welcome SUI😏 you later

1
Subscribe to my newsletter

Read articles from Daniel Nwachukwu Chigozirim directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Daniel Nwachukwu Chigozirim
Daniel Nwachukwu Chigozirim

Web3 Front-end developer || Smart contract developer || Tech enthusiast || Let's talk web 🕸️ || GitHub - http://github.com/Verifieddanny