Contrato Crowdfunding
Explicaci贸n
Este contrato de Crowdfunding permite a los usuarios financiar proyectos, pero s贸lo si alcanzan sus objetivos de financiaci贸n en un plazo determinado. Si se alcanza el objetivo, se puede invocar un mensaje de ejecuci贸n. Si no se alcanza, el contrato permitir谩 autom谩ticamente que cualquiera pueda reclamar sus fondos y/o reembolsar a otros.
Crear una instancia
Owner
: La persona que crea el contrato debe ser el propietario.
Denom
: especifica el tipo de tokens utilizados para la financiaci贸n.
Goal
: establece el objetivo de financiaci贸n en tokens.
Start
: Determina cu谩ndo comienza el per铆odo de financiaci贸n (puede ser inmediato o futuro).
Deadline: Especifica cu谩ndo se debe cumplir la meta de financiamiento (dentro de los 60 d铆as a partir de ahora, en el futuro).
Name
: El nombre del proyecto (menos de 32 caracteres).
Description
: Una breve descripci贸n del proyecto (menos de 256 caracteres).
Consultas
get_config
: devuelve detalles del proyecto como objetivo, fecha l铆mite, nombre y descripci贸n.
get_shares
: muestra las acciones de un usuario en el proyecto.
get_funders
: proporciona una lista de todos los financiadores y sus acciones.
get_funds
: revela el total de fondos recaudados hasta el momento.
Comportamiento
fund
: permite a los usuarios contribuir con tokens al proyecto (el proyecto debe iniciarse, no cerrarse y los tokens deben ser v谩lidos).
execute
: ejecuta el proyecto si se alcanza el objetivo de financiaci贸n (el proyecto debe estar cerrado y totalmente financiado).
refund
: Reembolsa a los contribuyentes si no se cumple el objetivo de financiaci贸n (el proyecto debe estar cerrado y parcialmente financiado).
claim
: Permite reclamar fondos del proyecto si se alcanza la meta (el proyecto debe estar cerrado y parcialmente financiado).
Estado
config
: Almacena la configuraci贸n del proyecto.
shares
: realiza un seguimiento de los recursos compartidos de todos los usuarios en el proyecto.
total_shares
: muestra el total de tokens recaudados.
execute_msg
: Contiene el mensaje que se ejecutar谩 si se logra el objetivo de financiaci贸n.
Ejemplo
Para crear un contrato de financiaci贸n colectiva con CosmWasm, puede crear los siguientes archivos: lib.rs contract.rs msg.rs error.rs state.rs helpers.rs reglas.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"),
})
);
}
}
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(())
};
Last updated