This site requires Javascript to be enabled.

Crowdfunding Contract

Explanation

This Crowdfunding contract enables users to fund projects, but only if they reach their funding goals by a set deadline. If the goal is reached, an execute message can be invoked. If it is not reached, the contract will automatically enable anyone to claim their funds and/or refund others.

Instantiate

Owner: The person creating the contract must be the owner.

Denom: Specifies the type of tokens used for funding.

Goal: Sets the funding target in tokens.

Start: Determines when the funding period starts (can be immediate or in the future).

Deadline: Specifies when the funding goal must be met (within 60 days from now, in the future).

Name: The project's name (less than 32 characters).

Description: A brief project description (less than 256 characters).

Queries

get_config: Returns project details like goal, deadline, name, and description.

get_shares: Shows a user's shares in the project.

get_funders: Provides a list of all funders and their shares.

get_funds: Reveals the total funds raised so far.

Actions

fund: Allows users to contribute tokens to the project (project must be started, not closed, and tokens must be valid).

execute: Executes the project if the funding goal is reached (project must be closed and fully funded).

refund: Refunds contributors if the funding goal is not met (project must be closed and partially funded).

claim: Allows claiming project funds if the goal is reached (project must be closed and partially funded).

State

config: Stores the project's configuration.

shares: Keeps track of all users' shares in the project.

total_shares: Shows the total tokens raised.

execute_msg: Contains the message to be executed if the funding goal is achieved.

Example

To create a crowdfunding contract with CosmWasm, you can create the following files: lib.rs contract.rs msg.rs error.rs state.rs helpers.rs rules.rs

lib.rs

pub mod contract;mod error;pub mod helpers;pub mod msg;pub mod rules;pub mod state;pub use crate::error::ContractError;

contract.rs

#[cfg(not(feature = "library"))]use cosmwasm_std::entry_point;use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, StdError, CosmosMsg, Empty, Order, BankMsg, coins, Addr, coin};use cw2::set_contract_version;use cw_storage_plus::Bound;use crate::error::ContractError;use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, QueryResponseWrapper, GetConfigResponse, GetSharesResponse, GetFundersResponse, GetTotalFundsResponse};use crate::state::{Config,CONFIG,SHARES,TOTAL_SHARES,EXECUTE_MSG};use crate::rules;const CONTRACT_NAME: &str = "crates.io:crowdfunding";const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");#[cfg_attr(not(feature = "library"), entry_point)]pub fn instantiate(    deps: DepsMut,    env: Env,    _info: MessageInfo,    msg: InstantiateMsg,) -> Result<Response, ContractError> {    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;    let config = Config {        owner: env.contract.address,        denom: msg.denom,        goal: msg.goal,        start: msg.start.unwrap_or(env.block.time),        deadline: msg.deadline,        name: msg.name,        description: msg.description,    };    config.validate()?;    CONFIG.save(deps.storage, &config)?;    TOTAL_SHARES.save(deps.storage, &Uint128::zero())?;    EXECUTE_MSG.save(deps.storage, &msg.execute_msg)?;    Ok(Response::new()        .add_attribute("action", "instantiate")        .add_attribute("name", config.name))}#[cfg_attr(not(feature = "library"), entry_point)]pub fn execute(    deps: DepsMut,    env: Env,    info: MessageInfo,    msg: ExecuteMsg,) -> Result<Response, ContractError> {    use ExecuteMsg::*;    match msg {        Fund {} => {            rules::HAS_STARTED(&deps, &env, &info)?;            rules::NOT_CLOSED(&deps, &env, &info)?;            rules::SENT_FUNDS(&deps, &env, &info)?;            try_fund(deps, env, info)        }        Execute {} => {            rules::IS_CLOSED(&deps, &env, &info)?;            rules::FULLY_FUNDED(&deps, &env, &info)?;            try_execute(deps, env, info)        }        Claim {} => {            rules::IS_CLOSED(&deps, &env, &info)?;            rules::NOT_FULLY_FUNDED(&deps, &env, &info)?;            try_claim(deps, env, info)        }        Refund {} => {            rules::IS_CLOSED(&deps, &env, &info)?;            rules::NOT_FULLY_FUNDED(&deps, &env, &info)?;            try_refund(deps, env, info)        }    }}pub fn try_fund(deps: DepsMut, _env: Env, info: MessageInfo) -> Result<Response, ContractError> {    let config = CONFIG.load(deps.storage)?;    let sent_funds = info        .funds        .iter()        .find_map(|v| {            if v.denom == config.denom {                Some(v.amount)            } else {                None            }        })        .unwrap_or_else(Uint128::zero);        SHARES        .update::<_, StdError>(deps.storage, info.sender, |shares| {            let mut shares = shares.unwrap_or_default();            shares += sent_funds;            Ok(shares)        })?;            TOTAL_SHARES        .update::<_, StdError>(deps.storage, |total_shares| {            let mut total_shares = total_shares;            total_shares += sent_funds;            Ok(total_shares)        })?;    Ok(Response::new())}pub fn try_execute(deps: DepsMut, _env: Env, _info: MessageInfo) -> Result<Response, ContractError> {    let execute_msg = EXECUTE_MSG        .load(deps.storage)?        .ok_or_else(|| StdError::generic_err("execute_msg not set".to_string()))?;    // execute can only run once ever.    EXECUTE_MSG.save(deps.storage, &None)?;    Ok(Response::new().add_message(execute_msg))}pub fn try_refund(deps: DepsMut, env: Env, _info: MessageInfo) -> Result<Response, ContractError> {    let config = CONFIG.load(deps.storage)?;    let contract_balance = deps        .querier        .query_balance(env.contract.address, config.denom.clone())?        .amount;    let total_shares = TOTAL_SHARES.load(deps.storage)?;    let user_shares = SHARES        .range(deps.storage, None, None, Order::Ascending)        // batch execute 30 transfers at a time        .take(30)        .collect::<Result<Vec<_>, _>>()?;    let mut next_shares = total_shares;    let msgs: Vec<CosmosMsg> = vec![];    for (addr, shares) in user_shares {        let refund_amount = contract_balance.multiply_ratio(shares, total_shares);        let _bank_transfer_msg = CosmosMsg::<Empty>::Bank(BankMsg::Send {            to_address: addr.to_string(),            amount: coins(refund_amount.u128(), config.denom.clone()),        });        SHARES.remove(deps.storage, addr);        next_shares -= shares;    }    TOTAL_SHARES.save(deps.storage, &next_shares)?;    Ok(Response::new().add_messages(msgs))}pub fn try_claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, ContractError> {    let config = CONFIG.load(deps.storage)?;    let contract_balance = deps        .querier        .query_balance(env.contract.address, config.denom.clone())?        .amount;    let total_shares = TOTAL_SHARES.load(deps.storage)?;    let user_shares = SHARES.load(deps.storage, info.sender.clone())?;    let mut next_total_shares = total_shares;    let refund_amount = contract_balance.multiply_ratio(user_shares, total_shares);    let bank_transfer_msg = CosmosMsg::<Empty>::Bank(BankMsg::Send {        to_address: info.sender.to_string(),        amount: coins(refund_amount.u128(), config.denom),    });    SHARES.remove(deps.storage, info.sender);    next_total_shares -= user_shares;    TOTAL_SHARES.save(deps.storage, &next_total_shares)?;    Ok(Response::new().add_message(bank_transfer_msg))}#[cfg_attr(not(feature = "library"), entry_point)]pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {    let output: StdResult<QueryResponseWrapper> = match msg {        QueryMsg::GetConfig {} => get_config(deps, env),        QueryMsg::GetShares { user } => get_shares(deps, env, user),        QueryMsg::GetFunders { limit, start_after } => {            get_funders(deps, env, limit, start_after)        }        QueryMsg::GetTotalFunds {} => get_funds(deps, env),    };    output?.to_binary()}pub fn get_config(deps: Deps, _env: Env) -> StdResult<QueryResponseWrapper> {    let config = CONFIG.load(deps.storage)?;    Ok(QueryResponseWrapper::GetConfigResponse(GetConfigResponse {        goal: coin(config.goal.u128(), config.denom),        deadline: config.deadline,        name: config.name,        description: config.description,    }))}pub fn get_shares(deps: Deps, _env: Env, address: String) -> StdResult<QueryResponseWrapper> {    let addr = deps.api.addr_validate(&address)?;    let shares = SHARES.load(deps.storage, addr)?;    Ok(QueryResponseWrapper::GetSharesResponse(GetSharesResponse {        shares,        address,    }))}pub fn get_funders(    deps: Deps,    _env: Env,    limit: Uint128,    start_after: Option<String>,) -> StdResult<QueryResponseWrapper> {    let start = start_after        .map(|s| deps.api.addr_validate(&s))        .transpose()?        .map(|addr| Bound::InclusiveRaw::<Addr>(addr.as_bytes().to_vec()));    let funders = SHARES        .range(deps.storage, start, None, Order::Ascending)        .take(limit.u128() as usize)        .collect::<Result<Vec<_>, _>>()?        .iter()        .map(|(addr, shares)| (addr.to_string(), *shares))        .collect::<Vec<(String, Uint128)>>();    Ok(QueryResponseWrapper::GetFundersResponse(        GetFundersResponse { funders },    ))}pub fn get_funds(deps: Deps, _env: Env) -> StdResult<QueryResponseWrapper> {    let funds = TOTAL_SHARES.load(deps.storage)?;    let config = CONFIG.load(deps.storage)?;    Ok(QueryResponseWrapper::GetTotalFundsResponse(        GetTotalFundsResponse {            total_funds: coin(funds.u128(), config.denom),        },    ))}#[cfg(test)]mod tests {    use super::*;    use cosmwasm_std::{        testing::{mock_dependencies, mock_env, mock_info},        coins, CosmosMsg, Empty, Uint128,    };    #[test]    fn test_instantiate() {        let mut deps = mock_dependencies();        let  env = mock_env();        let info = mock_info("creator", &coins(100, "earth"));        // Instantiate the contract        let msg = InstantiateMsg {            denom: "earth".to_string(),            goal: Uint128::new(100),            start: None,            deadline: env.block.time.plus_seconds(86400),            name: "Crowdfunding Campaign".to_string(),            description: "Test campaign".to_string(),            execute_msg: Some(CosmosMsg::Custom(Empty {})),        };        let res=instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(),0);             }     #[test]     fn test_fund(){        let mut deps = mock_dependencies();        let mut env = mock_env();        let info = mock_info("creator", &coins(100, "earth"));        // Instantiate the contract        let msg = InstantiateMsg {            denom: "earth".to_string(),            goal: Uint128::new(100),            start: None,            deadline: env.block.time.plus_seconds(86400),            name: "Crowdfunding Campaign".to_string(),            description: "Test campaign".to_string(),            execute_msg: Some(CosmosMsg::Custom(Empty {})),        };        let res=instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(),0);        env.block.time = env.block.time.plus_seconds(60);        // Execute with Fund case        let msg = ExecuteMsg::Fund {};        let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(), 0);     }     #[test]     fn test_execute(){        let mut deps = mock_dependencies();        let mut env = mock_env();        let info = mock_info("creator", &coins(100, "earth"));        // Instantiate the contract        let msg = InstantiateMsg {            denom: "earth".to_string(),            goal: Uint128::new(100),            start: None,            deadline: env.block.time.plus_seconds(86400),            name: "Crowdfunding Campaign".to_string(),            description: "Test campaign".to_string(),            execute_msg: Some(CosmosMsg::Custom(Empty {})),        };        let res=instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(),0);        env.block.time = env.block.time.plus_seconds(60);        // Execute with Fund case        let msg = ExecuteMsg::Fund {};        let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(), 0);        // Execute with Execute case        env.block.time = env.block.time.plus_seconds(86401);        let msg = ExecuteMsg::Execute {};        let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(), 1);        assert_eq!(res.messages[0].msg, CosmosMsg::Custom(Empty {}));     }     #[test]     fn test_refund(){        let mut deps = mock_dependencies();        let mut env = mock_env();        let info = mock_info("creator", &coins(80, "earth"));        // Instantiate the contract        let msg = InstantiateMsg {            denom: "earth".to_string(),            goal: Uint128::new(100),            start: None,            deadline: env.block.time.plus_seconds(86400),            name: "Crowdfunding Campaign".to_string(),            description: "Test campaign".to_string(),            execute_msg: Some(CosmosMsg::Custom(Empty {})),        };        let res=instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(),0);        // Execute with Fund case        let msg = ExecuteMsg::Fund {};        env.block.time = env.block.time.plus_seconds(60);        let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(), 0);         // Execute with Refund case         env.block.time = env.block.time.plus_seconds(86400);        let msg = ExecuteMsg::Refund {};        let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(), 0);     }     #[test]     fn test_claim()     {        let mut deps = mock_dependencies();        let mut env = mock_env();        let info = mock_info("creator", &coins(80, "earth"));        // Instantiate the contract        let msg = InstantiateMsg {            denom: "earth".to_string(),            goal: Uint128::new(100),            start: None,            deadline: env.block.time.plus_seconds(86400),            name: "Crowdfunding Campaign".to_string(),            description: "Test campaign".to_string(),            execute_msg: Some(CosmosMsg::Custom(Empty {})),        };        let res=instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(),0);        // Execute with Fund case        let msg = ExecuteMsg::Fund {};        env.block.time = env.block.time.plus_seconds(60);        let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(), 0);        // Execute with Claim case        env.block.time = env.block.time.plus_seconds(86400);        let msg = ExecuteMsg::Claim {};        let res = execute(deps.as_mut(), env.clone(), info.clone(), msg).unwrap();        assert_eq!(res.messages.len(), 1);        assert_eq!(            res.messages[0].msg,            CosmosMsg::Bank(BankMsg::Send {                to_address: "creator".to_string(),                amount: coins(0, "earth"),            })        );     }}

msg.rs

use cosmwasm_std::{to_binary, Binary, Coin, CosmosMsg, StdError, Timestamp, Uint128};use schemars::JsonSchema;use serde::{Deserialize, Serialize};#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]#[serde(rename_all = "snake_case")]pub struct InstantiateMsg {    pub denom: String,    pub goal: Uint128,    pub start: Option<Timestamp>,    pub deadline: Timestamp,    pub name: String,    pub description: String,    pub execute_msg: Option<CosmosMsg>,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum ExecuteMsg {    // fund the project with a given amount of tokens    // receives coins from `WasmExecuteMsg.funds`    Fund {},    // execute the project if the goal is reached    Execute {},    // refund the project if the goal is not reached    Refund {},    // claim the project's funds if the goal is reached    Claim {},}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum QueryMsg {    GetConfig {},    // * `get_shares`: returns a user's shares in the project.    GetShares {        user: String,    },    // returns a list of all funders and their shares.    GetFunders {        limit: Uint128,        start_after: Option<String>,    },    // returns total fund held by contract.    GetTotalFunds {},}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] // returns configpub struct GetConfigResponse {    pub goal: Coin,    pub deadline: Timestamp,    pub name: String,    pub description: String,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] // returns a user's shares in the project.pub struct GetSharesResponse {    pub address: String,    pub shares: Uint128,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] // returns a list of all funders and their shares.pub struct GetFundersResponse {    pub funders: Vec<(String, Uint128)>,}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] // Get Total Funds Responsepub struct GetTotalFundsResponse {    pub total_funds: Coin,}#[derive(Serialize, Clone, Debug, PartialEq, JsonSchema)]pub enum QueryResponseWrapper {    GetConfigResponse(GetConfigResponse),    GetSharesResponse(GetSharesResponse),    GetFundersResponse(GetFundersResponse),    GetTotalFundsResponse(GetTotalFundsResponse),}impl QueryResponseWrapper {    pub fn to_binary(&self) -> Result<Binary, StdError> {        match self {            QueryResponseWrapper::GetConfigResponse(x) => to_binary(x),            QueryResponseWrapper::GetSharesResponse(x) => to_binary(x),            QueryResponseWrapper::GetFundersResponse(x) => to_binary(x),            QueryResponseWrapper::GetTotalFundsResponse(x) => to_binary(x),        }    }}#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]#[serde(rename_all = "snake_case")]pub enum MigrateMsg {}

error.rs

use cosmwasm_std::StdError;use thiserror::Error;#[derive(Error, Debug)]pub enum ContractError {    #[error("{0}")]    Std(#[from] StdError),    #[error("Custom Error val: {val:?}")]    CustomError { val: String },}

state.rs

use schemars::JsonSchema;use serde::{Deserialize, Serialize};use cosmwasm_std::{Addr, CosmosMsg, StdError, Timestamp, Uint128};use cw_storage_plus::{Item, Map};#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]pub struct Config {    pub owner: Addr,    pub denom: String,    pub goal: Uint128,    pub start: Timestamp,    pub deadline: Timestamp,    pub name: String,    pub description: String,}impl Config {    pub fn validate(&self) -> Result<(), StdError> {        if self.goal <= Uint128::zero() {            return Err(StdError::generic_err(                "goal must be greater than 0".to_string(),            ));        }        if self.start >= self.deadline {            return Err(StdError::generic_err(                "start must be before deadline".to_string(),            ));        }        // description must be less than 256 characters        if self.description.len() > 256 {            return Err(StdError::generic_err(                "description must be less than 256 characters".to_string(),            ));        }        // title must be less than 32 characters        if self.name.len() > 32 {            return Err(StdError::generic_err(                "title must be less than 32 characters".to_string(),            ));        }        Ok(())    }}pub const CONFIG: Item<Config> = Item::new("config");pub const SHARES: Map<Addr,Uint128> = Map::new("shares");pub const TOTAL_SHARES: Item<Uint128> = Item::new("total_shares");pub const EXECUTE_MSG: Item<Option<CosmosMsg>> = Item::new("execute_msg");

helpers.rs

use schemars::JsonSchema;use serde::{Deserialize, Serialize};use cosmwasm_std::Addr;/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers/// for working with this. Rename it to your contract name.#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]pub struct CwTemplateContract(pub Addr);// impl CwTemplateContract {//     pub fn addr(&self) -> Addr {//         self.0.clone()//     }//     pub fn call<T: Into<ExecuteMsg>>(&self, msg: T) -> StdResult<CosmosMsg> {//         let msg = to_binary(&msg.into())?;//         Ok(WasmMsg::Execute {//             contract_addr: self.addr().into(),//             msg,//             funds: vec![],//         }//         .into())//     }//     /// Get Custom//     pub fn custom_query<Q, T, CQ>(&self, querier: &Q, val: String) -> StdResult<CustomResponse>//     where//         Q: Querier,//         T: Into<String>,//         CQ: CustomQuery,//     {//         let msg = QueryMsg::CustomMsg { val };//         let query = WasmQuery::Smart {//             contract_addr: self.addr().into(),//             msg: to_binary(&msg)?,//         }//         .into();//         let res: CustomResponse = QuerierWrapper::<CQ>::new(querier).query(&query)?;//         Ok(res)//     }// }

rules.rs

use cosmwasm_std::{DepsMut, Env, MessageInfo, StdError, Uint128};use crate::state::{CONFIG, TOTAL_SHARES};type Rule =    fn(deps: &DepsMut, env: &Env, info: &MessageInfo) -> Result<(), StdError>;pub const HAS_STARTED: Rule = |deps, env, _info| {    if CONFIG.load(deps.storage)?.start >= env.block.time {        return Err(StdError::generic_err(            "project has not started yet".to_string(),        ));    }    Ok(())};pub const NOT_CLOSED: Rule = |deps, env, _info| {    if CONFIG.load(deps.storage)?.deadline <= env.block.time {        return Err(StdError::generic_err("Project is closed"));    }    Ok(())};pub const SENT_FUNDS: Rule = |deps, _env, info| {    let denom = CONFIG.load(deps.storage)?.denom;    if info        .funds        .iter()        .find_map(|v| {            if v.denom == denom {                Some(v.amount)            } else {                None            }        })        .unwrap_or_else(Uint128::zero)        .is_zero()    {        return Err(StdError::generic_err("Amount must be positive"));    }    Ok(())};pub const FULLY_FUNDED: Rule = |deps, _env, _info| {    let config = CONFIG.load(deps.storage)?;    let goal = config.goal;    let _denom = config.denom;    let total_shares = TOTAL_SHARES.load(deps.storage)?;    if total_shares < goal {        return Err(StdError::generic_err(format!(            "Project must be fully funded: {} < {}",            total_shares, goal        )));    }    Ok(())};pub const IS_CLOSED: Rule = |deps, env, _info| {    if CONFIG.load(deps.storage)?.deadline > env.block.time {        return Err(StdError::generic_err("Project is open"));    }    Ok(())};pub const NOT_FULLY_FUNDED: Rule = |deps, _env, _info| {    let config = CONFIG.load(deps.storage)?;    let goal = config.goal;    let total_shares = TOTAL_SHARES.load(deps.storage)?;    if total_shares >= goal {        return Err(StdError::generic_err(format!(            "Project must not be fully funded: {} >= {}",            total_shares, goal        )));    }    Ok(())};

Credits: CosmWasm by example. You can check the code on Github or open it with VS code.

Drop Camp is here!

Join the queue and be one of the first to get in!.

Go Camping ↗