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.