- Docs
- Developers
- Crowdfunding
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.