Developers Home»How-to Guide»Simple Crowdfund

Simple Crowdfund

Note

This guide assumes that developers know how to create their own 'Errors' and 'Events' according to the pallet logic they're creating.

Goal

Build a pallet that controls multiple token accounts, storing data in child storage.

Use Cases

A simple on-chain crowdfunding app for participants to pool funds towards a common goal.

Overview

Using a child trie provides two advantages over using standard storage. First, it allows for removing the entirety of the trie is a single storage write when the fund is dispensed or dissolved. Second, it allows any contributor to prove that they contributed using a Merkle Proof. This guide demonstrates how to use one trie for each active crowdfund. Any user can start a crowdfund by specifying a goal amount for the crowdfund, an end time, and a beneficiary who will receive the pooled funds if the goal is reached by the end time. If the fund is not successful, it enters into a retirement period when contributors can reclaim their pledged funds. Finally, an unsuccessful fund can be dissolved, sending any remaining tokens to the user who dissolves it.

Information

To follow this guide and implement the crowdfunding capabilities in your own pallet, include the following dependencies in your pallet:

    use frame_support::{pallet_prelude::*, ensure, storage::child,
    traits::{Currency, ExistenceRequirement, Get, ReservableCurrency, WithdrawReasons},
    sp_runtime::{traits::{AccountIdConversion, Saturating, Zero, Hash},
    ModuleId}
    };
    use frame_system::{pallet_prelude::*, ensure_signed};
    use super::*;

Steps

1. Declaring your pallet's configuration traits

In addition to the ubiquitous Event type, this pallet will need:

  • Currency. The currency in which the crowdfunds will be denominated.
  • SubmissionDeposit. The amount to be held on deposit by the owner of a crowdfund.
  • MinContribution. The minimum amount that may be contributed into a crowdfund.
  • RetirementPeriod. The period of time in blocks during which contributors are able to withdraw their funds after an unsuccessful crowdfund ending.
/// The pallet's configuration trait.
#[pallet::config]
pub trait Config: frame_system::Config {
  type Event: From<Event<Self>> + IsType<<Self as frame_system::Config>::Event>;
    type Currency: ReservableCurrency<Self::AccountId>;
    type SubmissionDeposit: Get<BalanceOf<Self>>;
    type MinContribution: Get<BalanceOf<Self>>;
    type RetirementPeriod: Get<Self::BlockNumber>;
}

2. Create a custom metadata struct

Keep track of the constants in your pallet by creating a struct that stores metadata about each fund:

#[derive(Encode, Decode, Default, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub struct FundInfo<AccountId, Balance, BlockNumber> {
    /// The account that will recieve the funds if the campaign is successful.
    beneficiary: AccountId,
    /// The amount of deposit placed.
    deposit: Balance,
    /// The total amount raised.
    raised: Balance,
    /// Block number after which funding must have succeeded.
    end: BlockNumber,
    /// Upper bound on `raised`.
    goal: Balance,
}

3. Declare your storage items

Our storage items will need to keep track of which user contributed to what fund as well as how much they contributed. First, define the following types which will be used to declare our storage items:

pub type FundIndex = u32;
type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
type BalanceOf<T> = <<T as Config>::Currency as Currency<AccountIdOf<T>>>::Balance;
type FundInfoOf<T> = FundInfo<AccountIdOf<T>, BalanceOf<T>, <T as frame_system::Config>::BlockNumber>;

Now, use those types to create one StorageMap item that tracks the funds by index and a second, StorageValue, that keeps track of FundIndex:

#[pallet::storage]
#[pallet::getter(fn funds)]
/// Info on all of the funds.
pub(super) type Funds<T: Config> = StorageMap<
  _,
  Blake2_128Concat,
  FundIndex,
  FundInfoOf<T>,
  OptionQuery,
>;

    #[pallet::storage]
    #[pallet::getter(fn fund_count)]
    /// The total number of funds that have so far been allocated.
    pub(super) type FundCount<T: Config> = StorageValue<_, FundIndex, ValueQuery>;

4. Write child trie API helper functions

First, we'll need to create a function that provides the pallet's dispatchables with the account ID for the pot of funds. Inside impl<T: Config> Pallet<T>, write:

const PALLET_ID: ModuleId = ModuleId(*b"ex/cfund");
/**snip**/
pub fn fund_account_id(index: FundIndex) -> T::AccountId {
    PALLET_ID.into_sub_account(index)
    }

Then, we'll need to generate unique ChildInfo IDs:

pub fn id_from_index(index: FundIndex) -> child::ChildInfo {
  let mut buf = Vec::new();
  buf.extend_from_slice(b"crowdfnd");
  buf.extend_from_slice(&index.to_le_bytes()[..]);

  child::ChildInfo::new_default(T::Hashing::hash(&buf[..]).as_ref())
}

Finally, we can write the following helper functions that make use of the Child API:

  • pub fn contribution_put: record a contribution in the associated child trie using put

  • pub fn contribution_get: lookup a contribution in the associated child trie using get

  • pub fn contribution_kil: remove a contribution from an associated child trie using kill

  • pub fn crowdfund_kill: remove the entire record of contributions in the associated child trie in a single storage write using kill_storage

5. Write your dispatchable functions

The follow steps outline how to write the dispatchables for this pallet. After various checks within the dispatchables' logic, each function alters the Funds<T> storage map using its associated methods. Our pallet's create function also makes use of the FundInfo struct created in step 2.

Create a new fund

fn create:

  • create an imbalance variable using T::Currency::withdraw
  • update the Funds storage item using the FundInfo struct from step 2
  • deposit a Created event
Tip

Not yet familiar with using a struct in storage? Refer to this how-to guide to learn how.

Contribute funds to an existing fund

fn contribute:

  • perform preliminary safety checks using ensure!
  • add the contribution to the fund using T::Currency::transfer
  • record the contribution in the child trie using the helper function contribution_put
  • deposit a Contributed event

Withdraw full balance of a contributor to a fund

fn withdraw:

  • perform preliminary safety checks using ensure!
  • return funds by using T::Currency::resolve_into_existing
  • calculate new balances and update storage using child trie helper functions funds, contribution_get and contribution_kill
  • deposit Withdrew event

Dissolve an entire crowdfund after its retirement period has expired.

fn dissolve:

  • perform preliminary safety checks using ensure!
  • allow dissolver to collect funds by using T::Currency::resolve_creating for dissolver to collect funds
  • use the child trie helper function crowdfund_kill to remove contributor info from storage
  • deposit Dissolved event

Dispense a payment to the beneficiary of a successful crowdfund

fn dispense:

  • use T::Currency::resolve_creating for beneficiary and caller (separately) to collect funds
  • give initial deposit to account who calls this function as an incentive to clean up storage
  • remove the fund from storage using <Funds<T>>::remove(index); and Self::crowdfund_kill(index); to remove all contributors from storage in a single write

Examples

Resources

Rust docs

Last edit: on

Run into problems?
Let us Know