diff --git a/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/1.math.md b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/1.math.md new file mode 100644 index 00000000..7c63aef4 --- /dev/null +++ b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/1.math.md @@ -0,0 +1,408 @@ +--- +objectID: cosmwasm_by_example|math +title: Math +description: Learn how to perform math operations with CosmWasm. +parentSection: Developers +parentSectionPath: /developers +--- + +# Cosmwasm Maths Operations + +## Explanation + +In this section, you can see examples of a CosmWasm contract that implements simple maths operations like addition, substraction, multiplication, division, modulo and exponential. + +The following **execute** function takes a enum of **ExecuteMsg** which actually contains all the contract function and matches them with the function user is calling: +::highlight-card +```rust +pub fn execute( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Operations { a, b } => execute_operations(deps,a,b), +} +} +``` +:: +The **execute** function takes a enum of **ExecuteMsg** which actually contains all the contract function and matches them with the function user is calling, and then it calls **execute_operations** function: +::highlight-card +```rust +pub fn execute_operations(deps: DepsMut, a: u128, b: u128) -> Result { + // Checking if numbers are not zero + if a == 0 && b == 0 { + return Err(ContractError::CanNotBeZero()); + } + + // Addition + let addition_result = a + b; + + // Subtraction + let subtraction_result = a - b; + + // Multiplication + let multiplication_result = a * b; + + // Division + let division_result = a / b; + + // Modulo + let modulo_result = a % b; + + // Exponentiation + let exponent: u32 = 3; + let exponentiation_result: u128 = a.pow(exponent); + + // Create the response + let response = OperationsResponse { + addition_result, + subtraction_result, + multiplication_result, + division_result, + modulo_result, + exponentiation_result, + }; + + // Fetching the state + RESULT.load(deps.storage).unwrap(); + + // Update the state + RESULT.save(deps.storage, &response).unwrap(); + + let res = Response::new().add_attributes(vec![ + ("action", "operations"), + ("a", &a.to_string()), + ("b", &b.to_string()), + ("addition_res", &addition_result.to_string()), + ("substraction_res", &subtraction_result.to_string()), + ("multiplicationn_res", &multiplication_result.to_string()), + ("division_res", &division_result.to_string()), + ("modulo_res", &modulo_result.to_string()), + ("exponential_res", &exponentiation_result.to_string()), + ]); + + Ok(res) + } +``` +:: +The **execute_operations** function takes two parameters *a* and *b* for mathmatical operations and store the result in **RESULT** global state variable stored in **state.rs** : +::highlight-card +```rust +pub const RESULT: Item = Item::new("result"); +``` +:: + + + +You can use the following query endpoint is getting the result of mathmatical operations: . +::highlight-card +```rust +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::GetResponse {} => get_response(deps), + } +} +``` +:: + +The above query function takes a enum of **QueryMsg** which actually contains all the contract query function and matches them with the function user is calling. In our case GetResponse. Then it calls **get_response** function: +::highlight-card +```rust +pub fn get_response(deps: Deps) -> Result { + let result = RESULT.load(deps.storage)?; + + to_binary(&result) +} +``` +:: +The above **get_response** function return the result of the mathmatical operation. + + +## Example +To use math operations with CosmWasm, you can create the following files: +lib.rs +contract.rs +msg.rs +error.rs +state.rs + + +### lib.rs +::highlight-card +```rust +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; +``` +:: + + +### contract.rs + +::highlight-card +```rust +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError, +}; +use cw2::set_contract_version; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::{OperationsResponse, RESULT}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:cosmwasm-math"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + /* Deps allows to access: + 1. Read/Write Storage Access + 2. General Blockchain APIs + 3. The Querier to the blockchain (raw data queries) */ + deps: DepsMut, + /* env gives access to global variables which represent environment information. + For exaample: + - Block Time/Height + - contract address + - Transaction Info */ + _env: Env, + /* Message Info gives access to information used for authorization. + 1. Funds sent with the message. + 2. The message sender (signer). */ + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result { + /* Instantiating the state that will be stored to the blockchain */ + let operation_response = OperationsResponse { + addition_result: 0, + subtraction_result: 0, + multiplication_result: 0, + division_result: 0, + modulo_result: 0, + exponentiation_result: 0, + }; + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION).unwrap(); + // Save the stete in deps.storage which creates a storage for contract data on the blockchain. + RESULT.save(deps.storage, &operation_response).unwrap(); + + Ok(Response::new().add_attribute("method", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Operations { a, b } => execute::execute_operations(deps, a, b), + } +} + +pub mod execute { + use super::*; + + pub fn execute_operations(deps: DepsMut, a: u128, b: u128) -> Result { + // Checking if numbers are not zero + if a == 0 && b == 0 { + return Err(ContractError::CanNotBeZero()); + } + + // Addition + let addition_result = a + b; + + // Subtraction + let subtraction_result = a - b; + + // Multiplication + let multiplication_result = a * b; + + // Division + let division_result = a / b; + + // Modulo + let modulo_result = a % b; + + // Exponentiation + let exponent: u32 = 3; + let exponentiation_result: u128 = a.pow(exponent); + + // Create the response + let response = OperationsResponse { + addition_result, + subtraction_result, + multiplication_result, + division_result, + modulo_result, + exponentiation_result, + }; + + // Fetching the state + RESULT.load(deps.storage).unwrap(); + + // Update the state + RESULT.save(deps.storage, &response).unwrap(); + + let res = Response::new().add_attributes(vec![ + ("action", "operations"), + ("a", &a.to_string()), + ("b", &b.to_string()), + ("addition_res", &addition_result.to_string()), + ("substraction_res", &subtraction_result.to_string()), + ("multiplicationn_res", &multiplication_result.to_string()), + ("division_res", &division_result.to_string()), + ("modulo_res", &modulo_result.to_string()), + ("exponential_res", &exponentiation_result.to_string()), + ]); + + Ok(res) + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + match msg { + QueryMsg::GetResponse {} => query::get_response(deps), + } +} + +pub mod query { + + use super::*; + + pub fn get_response(deps: Deps) -> Result { + let result = RESULT.load(deps.storage)?; + + to_binary(&result) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::{coins, from_binary}; + + #[test] + fn proper_initialization() { + let mut deps = mock_dependencies(); + + let msg = InstantiateMsg {}; + let info = mock_info("creator", &coins(1000, "earth")); + + // we can just call .unwrap() to assert this was a success + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // it worked, let's query the state + let res = query(deps.as_ref(), mock_env(), QueryMsg::GetResponse {}).unwrap(); + let value: OperationsResponse = from_binary(&res).unwrap(); + + assert_eq!(0, value.addition_result); + assert_eq!(0, value.subtraction_result); + assert_eq!(0, value.multiplication_result); + assert_eq!(0, value.division_result); + assert_eq!(0, value.modulo_result); + assert_eq!(0, value.exponentiation_result); + } + + #[test] + fn increment() { + let mut deps = mock_dependencies(); + + let msg = InstantiateMsg {}; + let info = mock_info("creator", &coins(2, "token")); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // testing operation function + let info = mock_info("anyone", &coins(2, "token")); + let msg = ExecuteMsg::Operations { a: 5, b: 5 }; + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // should get basic math operation for 5 and 5 + let res = query(deps.as_ref(), mock_env(), QueryMsg::GetResponse { }).unwrap(); + let value: OperationsResponse = from_binary(&res).unwrap(); + assert_eq!(10, value.addition_result); + assert_eq!(0, value.subtraction_result); + assert_eq!(25, value.multiplication_result); + assert_eq!(1, value.division_result); + assert_eq!(0, value.modulo_result); + assert_eq!(125, value.exponentiation_result); + } +} + +``` +:: + +### msg.rs + +::highlight-card +```rust +use cosmwasm_schema::{cw_serde, QueryResponses}; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + Operations { a: u128, b: u128 }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(u128)] + GetResponse {}, +} + +``` +:: + + +### error.rs +::highlight-card +```rust +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("Numbers can not be zero")] + CanNotBeZero(), +} +``` +:: + +### state.rs +::highlight-card +```rust +use cosmwasm_schema::cw_serde; +use cw_storage_plus::Item; + +#[cw_serde] +pub struct OperationsResponse { + pub addition_result: u128, + pub subtraction_result: u128, + pub multiplication_result: u128, + pub division_result: u128, + pub modulo_result: u128, + pub exponentiation_result: u128, +} +// Mapping of result of two numbers + +pub const RESULT: Item = Item::new("result"); + +``` +:: +--------- +*Credits*: CosmWasm by example. +You can check the code on Github or open it with VS code. \ No newline at end of file diff --git a/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/2.instantiation.md b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/2.instantiation.md new file mode 100644 index 00000000..c2b96058 --- /dev/null +++ b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/2.instantiation.md @@ -0,0 +1,302 @@ +--- +objectID: cosmwasm_by_example|instantiation +title: Instatiating cw contracts +description: Learn how to instantiate CosmWasm contracts. +parentSection: Developers +parentSectionPath: /developers +--- + +# Instantiate a CosmWasm contract +This section describes the process of instantiating a smart contract, by showing the **InstantiateMsg** structure passed during contract creation and the **Instantiate** function that runs upon contract execution. + +## Explanation + +### InstantiateMsg +**InstantiateMsg** is the initialization message, and deals with the variables provided when the contract gets instantiated. + + +This function can be found in the **msg.rs** +::highlight-card +```rust + +/// Variables for contract instantiation. +/// Exclude variables susceptible to replay attacks or secret data. +pub struct InstantiateMsg { + pub sent_message: String, +} +``` +:: + +::alert{variant="info"} +Make sure that sensitive data prone to replay attacks isn't incorporated. +#title +Info +:: + + +### Instantiate +On contract creation, the instantiate function sets the initial state of the smart contract, conducts necessary checks, and can function akin to an execute message. + +This function can be found in the **contract.rs** +::highlight-card +```rust +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + // Initialize contract's state. + let state = State { + global_var: msg.sent_message, + }; + // Save state to the blockchain. + STATE.save(deps.storage, &state)?; + Ok(Response::new().add_attribute("instantiated", "true")) +} +``` +:: + + +## Example + +To instantiate contracts with CosmWasm, you can create the following files: +lib.rs +contract.rs +msg.rs +error.rs +state.rs +helpers.rs + +### lib.rs + +::highlight-card +```rust +pub mod contract; +mod error; +pub mod helpers; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; + +``` +:: + +### contract.rs + +::highlight-card +```rust +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, to_binary}; +// use cw2::set_contract_version; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::{State, STATE}; + +/* +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:instantiation"; +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 { + /* Code is executed at contract creation and accepts an `InstantiateMsg` + Used to set initial (default) state variables of the smart contract + Can perform checks and acts like an execute message. + */ + let state = State { + global_var: msg.sent_message, + }; + // Pushes the data to blockchain storage + STATE.save(deps.storage, &state)?; + Ok(Response::new() + .add_attribute("instantiated", "true")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: ExecuteMsg, +) -> Result { + unimplemented!() +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GetMessage {} => to_binary(&query::message(deps)?), + } + +} + +pub mod query { + + use crate::msg::GetMessageResponse; + + use super::*; + + pub fn message(deps: Deps) -> StdResult { + let state = STATE.load(deps.storage)?; + Ok(GetMessageResponse { message: state.global_var }) + } +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::{testing::{mock_dependencies, mock_env, mock_info}, Addr, from_binary}; + + use crate::msg::GetMessageResponse; + + use super::*; + + #[test] + fn instantiate_test() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = InstantiateMsg { sent_message: "Hey!".to_string()}; + let info = mock_info("creator", &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + + // Query data from blockchain + let res = query(deps.as_ref(), env, QueryMsg::GetMessage { }).unwrap(); + let message: GetMessageResponse = from_binary(&res).unwrap(); + assert_eq!("Hey!".to_string(), message.message); + } +} + +``` +:: + + +### msg.rs + +::highlight-card +```rust +use cosmwasm_schema::{cw_serde, QueryResponses}; + +#[cw_serde] +/* +variables to be passed to contract at instantiation. +secret variables or any variable that is open to a replay attack should not be +part of the InstantiateMsg +*/ +pub struct InstantiateMsg { + pub sent_message: String +} + +#[cw_serde] +pub enum ExecuteMsg {} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(GetMessageResponse)] + GetMessage {} +} + +#[cw_serde] +pub struct GetMessageResponse { + pub message: String +} +``` +:: + +### error.rs + +::highlight-card +```rust +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. +} + +``` +:: + +### state.rs + +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cw_storage_plus::Item; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct State { + pub global_var: String, +} + +pub const STATE: Item = Item::new("state"); +``` +:: + + +### helpers.rs + +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdResult, WasmMsg}; + +use crate::msg::ExecuteMsg; + +/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers +/// for working with this. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct CwTemplateContract(pub Addr); + +impl CwTemplateContract { + pub fn addr(&self) -> Addr { + self.0.clone() + } + + pub fn call>(&self, msg: T) -> StdResult { + let msg = to_binary(&msg.into())?; + Ok(WasmMsg::Execute { + contract_addr: self.addr().into(), + msg, + funds: vec![], + } + .into()) + } +} +``` +:: + + + + + + +--------- +*Credits*: CosmWasm by example. +You can check the code on Github or open it with VS code. + + + + + + diff --git a/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/3.timelock.md b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/3.timelock.md new file mode 100644 index 00000000..85337b06 --- /dev/null +++ b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/3.timelock.md @@ -0,0 +1,1223 @@ +--- +objectID: cosmwasm_by_example|timelock +title: Timelocks +description: Learn how to use timelocks in CosmWasm. +parentSection: Developers +parentSectionPath: /developers +--- + +# Timelock +## Explanation +A timelock smart contract introduces a delay mechanism for executing function calls on other smart contracts. It establishes a predefined minimum time delay before a scheduled operation can be executed. + +While Timelock is not strictly a MultiSig/Voting Contract, it aligns closely with the principles of CW3-spec compliant contracts. Instead of immediate execution, addresses can only propose or schedule operations, which then undergo a delay before final execution is allowed. + +### Instantiation +Upon deploying the Timelock contract, you can set: + +A minimum time delay and dddresses to act as Administrators and Proposers. +Operations can only be scheduled by proposers if their execution time exceeds the set delay. +### Administrators: + +Administrators handle initial configuration and ensure compatibility with potential target contracts. +By default, the contract initiator becomes the administrator if no other addresses are provided. +Post configuration, administrators can freeze the Timelock, making it immutable. This action is irrevocable and can render the contract unusable. +### Proposers + +Proposers schedule operations to be executed after the delay. +Ensure that the Timelock contract has necessary permissions on target contracts. +Specify executor addresses responsible for the final operation execution on the target contract. +If no executors is specified, any address can execute once the time arrives. +::alert{variant="info"} +Scheduling doesn't guarantee execution. Scheduled operations can be cancelled by the proposer before execution. Thus, choosing proposers is crucial. +#title +Info +:: + + +### InstantiateMsg +::highlight-card +```rust + +pub struct InstantiateMsg { + pub admins: Option>, + pub proposers: Vec, + pub min_delay: Duration, +} +Query +pub enum QueryMsg { + GetOperationStatus { + operation_id: Uint64, + }, + + GetExecutionTime { + operation_id: Uint64, + }, + + GetAdmins {}, + + GetOperations { + start_after: Option, + limit: Option, + }, + + GetMinDelay {}, + + GetProposers {}, + + GetExecutors { + operation_id: Uint64, + }, +} +``` +:: + + +### ExecuteMsg +::highlight-card +```rust + +pub enum ExecuteMsg { + Schedule { + target_address: String, + data: Binary, + title: String, + description: String, + execution_time: Scheduled, + executors: Option>, + }, + + Cancel { + operation_id: Uint64, + }, + + Execute { + operation_id: Uint64, + }, + + RevokeAdmin { + admin_address: String, + }, + + AddProposer { + proposer_address: String, + }, + + RemoveProposer { + proposer_address: String, + }, + + UpdateMinDelay { + new_delay: Duration, + }, + + Freeze {}, +} +``` +:: +## Example +To create a timelocks contract with CosmWasm, you can create the following files: +lib.rs +contract.rs +msg.rs +error.rs +state.rs + + +### lib.rs +:highlight-card +```rust +pub mod contract; +mod error; +pub mod helpers; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; + +``` +:: +### contract.rs +::highlight-card +```rust +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::WasmMsg::Execute; +use cosmwasm_std::{ + to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, + StdResult, Uint64, +}; +use cw2::set_contract_version; +use cw_storage_plus::Bound; +use cw_utils::{Duration, Scheduled}; +use std::ops::Add; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, OperationListResponse, QueryMsg}; +use crate::state::{Operation, OperationStatus, Timelock, CONFIG, OPERATION_LIST, OPERATION_SEQ}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:timelock"; +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 { + let mut admins = vec![]; + match msg.admins { + None => { + admins.push(info.sender.clone()); + } + Some(admin_list) => { + for admin in admin_list { + admins.push(deps.api.addr_validate(&admin)?); + } + } + } + admins.push(env.contract.address); + + let mut proposers = vec![]; + for proposer in msg.proposers { + proposers.push(deps.api.addr_validate(&proposer)?); + } + + let timelock = Timelock { + min_time_delay: msg.min_delay, + proposers, + admins, + frozen: false, + }; + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + OPERATION_SEQ.save(deps.storage, &Uint64::zero())?; + CONFIG.save(deps.storage, &timelock)?; + + Ok(Response::new() + .add_attribute("Method: ", "instantiate") + .add_attribute("Admin: ", info.sender) + .add_attribute( + "Proposers: ", + timelock + .proposers + .into_iter() + .map(|item| item.to_string()) + .collect::(), + ) + .add_attribute("minTimeDelay: ", timelock.min_time_delay.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Schedule { + target_address, + data, + title, + description, + execution_time, + executors, + } => execute_schedule( + deps, + _env, + info, + target_address, + data, + title, + description, + execution_time, + executors, + ), + ExecuteMsg::Execute { operation_id } => execute_execute(deps, _env, info, operation_id), + ExecuteMsg::Cancel { operation_id } => execute_cancel(deps, _env, info, operation_id), + ExecuteMsg::RevokeAdmin { admin_address } => { + execute_revoke_admin(deps, _env, info, admin_address) + } + ExecuteMsg::AddProposer { proposer_address } => { + execute_add_proposer(deps, _env, info, proposer_address) + } + ExecuteMsg::RemoveProposer { proposer_address } => { + execute_remove_proposer(deps, _env, info, proposer_address) + } + ExecuteMsg::UpdateMinDelay { new_delay } => { + execute_update_min_delay(deps, _env, info, new_delay) + } + ExecuteMsg::Freeze {} => execute_freeze(deps, _env, info), + } +} + +/*eslint too-many-arguments-threshold:9 */ +#[allow(clippy::too_many_arguments)] +pub fn execute_schedule( + deps: DepsMut, + env: Env, + info: MessageInfo, + target_address: String, + data: Binary, + title: String, + description: String, + execution_time: Scheduled, + executor_list: Option>, +) -> Result { + let sender = deps.api.addr_validate(&info.sender.to_string())?; + let target = deps.api.addr_validate(&target_address)?; + + let timelock = CONFIG.load(deps.storage)?; + if !(timelock.proposers.contains(&sender)) { + return Err(ContractError::Unauthorized {}); + } + + if Scheduled::AtTime(env.block.time).add(timelock.min_time_delay)? > execution_time { + return Err(ContractError::MinDelayNotSatisfied {}); + } + + let id = OPERATION_SEQ.update::<_, StdError>(deps.storage, |id| Ok(id.add(Uint64::new(1))))?; + + let mut executors = None; + match executor_list { + None => {} + Some(list) => { + let mut checked_executors = vec![]; + for executor in list { + checked_executors.push(deps.api.addr_validate(&executor)?); + } + executors = Option::from(checked_executors); + } + } + + let new_operation = Operation { + id, + status: OperationStatus::Pending, + proposer: sender, + executors, + execution_time, + target, + data, + title, + description, + }; + OPERATION_LIST.save(deps.storage, id.u64(), &new_operation)?; + + Ok(Response::new() + .add_attribute("Schedule ", "success") + .add_attribute("Operation ID: ", id) + .add_attribute("Proposer: ", new_operation.proposer) + .add_attribute("Target Address: ", new_operation.target.to_string()) + .add_attribute("Execution Time: ", new_operation.execution_time.to_string())) +} + +pub fn execute_execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + operation_id: Uint64, +) -> Result { + let mut operation = OPERATION_LIST.load(deps.storage, operation_id.u64())?; + + //is delay ended + if !operation.execution_time.is_triggered(&env.block) { + return Err(ContractError::Unexpired {}); + } + //has executer list if so sender is in it + if operation.executors.is_some() + && !operation + .executors + .clone() + .map(|c| c.contains(&info.sender)) + .unwrap() + { + return Err(ContractError::Unauthorized {}); + } + + if operation.status == OperationStatus::Done { + return Err(ContractError::Executed {}); + } + + //change operation status + operation.status = OperationStatus::Done; + OPERATION_LIST.save(deps.storage, operation_id.u64(), &operation)?; + + Ok(Response::new() + .add_message(CosmosMsg::Wasm(Execute { + contract_addr: operation.target.to_string(), + msg: operation.data, + funds: vec![], + })) + .add_attribute("executor", &info.sender.to_string())) +} + +pub fn execute_cancel( + deps: DepsMut, + _env: Env, + info: MessageInfo, + operation_id: Uint64, +) -> Result { + let operation = OPERATION_LIST.load(deps.storage, operation_id.u64())?; + + if operation.status == OperationStatus::Done { + return Err(ContractError::NotDeletable {}); + } + + if operation.proposer != info.sender { + return Err(ContractError::Unauthorized {}); + } + + OPERATION_LIST.remove(deps.storage, operation_id.u64()); + + Ok(Response::new() + .add_attribute("Method", "cancel") + .add_attribute("sender", &info.sender.to_string()) + .add_attribute("operation_id", operation_id.to_string()) + .add_attribute("Result", "Success")) +} + +pub fn execute_revoke_admin( + deps: DepsMut, + _env: Env, + info: MessageInfo, + admin_address: String, +) -> Result { + let mut timelock = CONFIG.load(deps.storage)?; + if timelock.frozen { + return Err(ContractError::TimelockFrozen {}); + } + if !timelock.admins.contains(&info.sender) { + return Err(ContractError::Unauthorized {}); + } + + let admin_address = deps.api.addr_validate(&admin_address)?; + + let index = timelock + .admins + .iter() + .position(|x| *x == admin_address.clone()) + .ok_or(ContractError::NotFound { + address: admin_address.clone().to_string(), + })?; + + timelock.admins.remove(index); + CONFIG.save(deps.storage, &timelock)?; + Ok(Response::new() + .add_attribute("Method", "revoke admin") + .add_attribute("sender", &info.sender) + .add_attribute("Admin to revoke", admin_address) + .add_attribute("Result", "Success")) +} + +pub fn execute_add_proposer( + deps: DepsMut, + _env: Env, + info: MessageInfo, + proposer_address: String, +) -> Result { + let mut timelock = CONFIG.load(deps.storage)?; + + if timelock.frozen { + return Err(ContractError::TimelockFrozen {}); + } + + if !timelock.admins.contains(&info.sender) { + return Err(ContractError::Unauthorized {}); + } + + let proposer_address = deps.api.addr_validate(&proposer_address)?; + + //is in proposers list + if timelock.proposers.contains(&proposer_address) { + return Err(ContractError::AlreadyContainsProposerAddress {}); + } + + timelock.proposers.push(proposer_address); + CONFIG.save(deps.storage, &timelock)?; + Ok(Response::new() + .add_attribute("Method", "add_proposer") + .add_attribute("sender", &info.sender) + .add_attribute("Result", "Success")) +} + +pub fn execute_remove_proposer( + deps: DepsMut, + _env: Env, + info: MessageInfo, + proposer_address: String, +) -> Result { + let mut timelock = CONFIG.load(deps.storage)?; + + if timelock.frozen { + return Err(ContractError::TimelockFrozen {}); + } + + if !timelock.admins.contains(&info.sender) { + return Err(ContractError::Unauthorized {}); + } + + let proposer_address = deps.api.addr_validate(&proposer_address)?; + //is in proposers + let index = timelock + .proposers + .iter() + .position(|x| *x == proposer_address.clone()) + .ok_or(ContractError::NotFound { + address: proposer_address.clone().to_string(), + })?; + + timelock.proposers.remove(index); + CONFIG.save(deps.storage, &timelock)?; + Ok(Response::new() + .add_attribute("Method", "remove_proposer") + .add_attribute("sender", &info.sender) + .add_attribute("Result", "Success")) +} + +pub fn execute_update_min_delay( + deps: DepsMut, + _env: Env, + info: MessageInfo, + new_delay: Duration, +) -> Result { + let mut timelock = CONFIG.load(deps.storage)?; + + if timelock.frozen { + return Err(ContractError::TimelockFrozen {}); + } + + if !timelock.admins.contains(&info.sender) { + return Err(ContractError::Unauthorized {}); + } + + timelock.min_time_delay = new_delay; + + CONFIG.save(deps.storage, &timelock)?; + Ok(Response::new() + .add_attribute("Method", "Update Min Delay") + .add_attribute("Sender", &info.sender.to_string()) + .add_attribute("New Min Delay", timelock.min_time_delay.to_string()) + .add_attribute("Result", "Success")) +} +pub fn execute_freeze( + deps: DepsMut, + _env: Env, + info: MessageInfo, +) -> Result { + let mut timelock = CONFIG.load(deps.storage)?; + + if timelock.frozen { + return Err(ContractError::TimelockFrozen {}); + } + + if !timelock.admins.contains(&info.sender) { + return Err(ContractError::Unauthorized {}); + } + + timelock.frozen = true; + + CONFIG.save(deps.storage, &timelock)?; + + Ok(Response::new() + .add_attribute("Method", "freeze") + .add_attribute("sender", &info.sender) + .add_attribute("Result", "Success")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GetOperationStatus { operation_id } => { + to_binary(&query_get_operation_status(deps, operation_id)?) + } + QueryMsg::GetExecutionTime { operation_id } => { + to_binary(&query_get_execution_time(deps, operation_id)?) + } + QueryMsg::GetAdmins {} => to_binary(&query_get_admins(deps)?), + QueryMsg::GetOperations { start_after, limit } => { + to_binary(&query_get_operations(deps, start_after, limit)?) + } + QueryMsg::GetMinDelay {} => to_binary(&query_get_min_delay(deps)?), + QueryMsg::GetProposers {} => to_binary(&query_get_proposers(deps)?), + QueryMsg::GetExecutors { operation_id } => { + to_binary(&query_get_executors(deps, operation_id)?) + } + } +} + +pub fn query_get_operation_status(deps: Deps, operation_id: Uint64) -> StdResult { + let operation = OPERATION_LIST.load(deps.storage, operation_id.u64())?; + Ok(operation.status) +} + +pub fn query_get_execution_time(deps: Deps, operation_id: Uint64) -> StdResult { + let operation = OPERATION_LIST.load(deps.storage, operation_id.u64())?; + Ok(operation.execution_time.to_string()) +} + +pub fn query_get_admins(deps: Deps) -> StdResult> { + let timelock = CONFIG.load(deps.storage)?; + Ok(timelock.admins) +} + +// settings for pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +pub fn query_get_operations( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.map(Bound::exclusive); + let operations: StdResult> = OPERATION_LIST + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect(); + + let res = OperationListResponse { + operationList: operations?.into_iter().map(|l| l.1.into()).collect(), + }; + Ok(res) +} + +pub fn query_get_min_delay(deps: Deps) -> StdResult { + let timelock = CONFIG.load(deps.storage)?; + Ok(timelock.min_time_delay.to_string()) +} + +pub fn query_get_proposers(deps: Deps) -> StdResult> { + let timelock = CONFIG.load(deps.storage)?; + Ok(timelock.proposers) +} + +pub fn query_get_executors(deps: Deps, operation_id: Uint64) -> StdResult> { + let operation = OPERATION_LIST.load(deps.storage, operation_id.u64())?; + Ok(operation.executors.unwrap_or_default()) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::Timestamp; + use cw_utils::Scheduled; + + #[test] + fn test_no_executers() { + let mut deps = mock_dependencies(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100); + let msg = InstantiateMsg { + admins: Option::Some(vec!["owner".to_string(), "new_one".to_string()]), + proposers: vec!["prop1".to_string(), "prop2".to_string()], + min_delay: Duration::Time(10), + }; + let info = mock_info("creator", &[]); + let description = "test desc".to_string(); + let title = "Title Example ".to_string(); + // instantiate + let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + println!("{:?}", res); + + let data = to_binary(&"data").unwrap(); + // try Schedule() with sender "creator" + let res = execute_schedule( + deps.as_mut(), + env.clone(), + info.clone(), + "target".to_string(), + data.clone(), + title.clone(), + description.clone(), + Scheduled::AtTime(Timestamp::from_seconds(10)), + Option::None, + ) + .unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + //change sender to prop1 + let info = mock_info("prop1", &[]); + //try Schedule() sender "prop1" execution_time < env.block.time + let res = execute_schedule( + deps.as_mut(), + env.clone(), + info.clone(), + "target".to_string(), + data.clone(), + title.clone(), + description.clone(), + Scheduled::AtTime(Timestamp::from_seconds(1)), + Option::None, + ) + .unwrap_err(); + assert_eq!(res, ContractError::MinDelayNotSatisfied {}); + + //Schedule() sender "prop1" execution_time > env.block.time && min_delay_time > execution_time - env.block.time + let res = execute_schedule( + deps.as_mut(), + env.clone(), + info.clone(), + "target".to_string(), + data.clone(), + title.clone(), + description.clone(), + Scheduled::AtTime(Timestamp::from_seconds(120)), + Option::None, + ) + .unwrap(); + println!("{:?}", res); + + let res = query_get_execution_time(deps.as_ref(), Uint64::new(1)); + println!("{:?}, {}", res, env.block.time); + + //try Execute() sender "prop1" execution_time > env.block.time + let res = + execute_execute(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap_err(); + assert_eq!(res, ContractError::Unexpired {}); + + //time pass + env.block.time = Timestamp::from_seconds(120); + //try Execute() sender "prop1" execution_time <= env.block.time executors "none" + let res = + execute_execute(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap(); + println!("{:?}", res); + } + + #[test] + fn test_with_executors() { + let mut deps = mock_dependencies(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100); + let msg = InstantiateMsg { + admins: Option::Some(vec!["owner".to_string(), "newone".to_string()]), + proposers: vec!["prop1".to_string(), "prop2".to_string()], + min_delay: Duration::Time(10), + }; + let info = mock_info("creator", &[]); + let title = "Title Example ".to_string(); + + // instantiate + let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + println!("{:?}", res); + + let data = to_binary(&"data").unwrap(); + let description = "test desc".to_string(); + //change sender to prop1 + let info = mock_info("prop1", &[]); + + //Schedule() sender "prop1" execution_time > env.block.time && min_delay_time > execution_time - env.block.time + let res = execute_schedule( + deps.as_mut(), + env.clone(), + info.clone(), + "target".to_string(), + data.clone(), + title.clone(), + description.clone(), + Scheduled::AtTime(Timestamp::from_seconds(120)), + Option::Some(vec!["exec1".to_string(), "exec2".to_string()]), + ) + .unwrap(); + println!("{:?}", res); + + let res = + query_get_operations(deps.as_ref(), Option::Some(0u64), Option::Some(1u32)).unwrap(); + println!("{:?}", res); + //time pass + env.block.time = Timestamp::from_seconds(120); + + //try Execute() sender "prop1" execution_time <= env.block.time executors "exec1, exec2" + let res = + execute_execute(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + let info = mock_info("exec1", &[]); + //Execute() sender "exec1" execution_time <= env.block.time executors "exec1, exec2" + let res = + execute_execute(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap(); + println!("{:?}", res); + } + + #[test] + fn test_cancel() { + let mut deps = mock_dependencies(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100); + let msg = InstantiateMsg { + admins: Option::Some(vec!["owner".to_string(), "newone".to_string()]), + proposers: vec!["prop1".to_string(), "prop2".to_string()], + min_delay: Duration::Time(10), + }; + let info = mock_info("creator", &[]); + let title = "Title Example ".to_string(); + + // instantiate + let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + println!("{:?}", res); + + let data = to_binary(&"data").unwrap(); + let description = "test desc".to_string(); + + //change sender to prop1 + let info = mock_info("prop1", &[]); + + //Schedule() sender "prop1" execution_time > env.block.time && min_delay_time > execution_time - env.block.time + let res = execute_schedule( + deps.as_mut(), + env.clone(), + info.clone(), + "target".to_string(), + data.clone(), + title.clone(), + description.clone(), + Scheduled::AtTime(Timestamp::from_seconds(120)), + Option::None, + ) + .unwrap(); + println!("{:?}", res); + + //time pass + env.block.time = Timestamp::from_seconds(120); + + //Execute() sender "prop1" executors "" + let res = + execute_execute(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap(); + println!("{:?}", res); + + //try Cancel() sender "prop1" operation_id "1" status "OperationStatus::Done" + let res = + execute_cancel(deps.as_mut(), env.clone(), info.clone(), Uint64::new(1)).unwrap_err(); + assert_eq!(res, ContractError::NotDeletable {}); + + //Schedule() sender "prop1" + let res = execute_schedule( + deps.as_mut(), + env.clone(), + info.clone(), + "target".to_string(), + data.clone(), + title.clone(), + description.clone(), + Scheduled::AtTime(Timestamp::from_seconds(140)), + Option::None, + ) + .unwrap(); + println!("{:?}", res); + + //Cancel() sender "prop1" operation_id "2" status "OperationStatus::Pending" + let res = execute_cancel(deps.as_mut(), env.clone(), info.clone(), Uint64::new(2)).unwrap(); + println!("{:?}", res); + + //try Cancel() sender "nobody" operation_id "2" admin "creator" proposers "prop1, prop2" + let res = + execute_cancel(deps.as_mut(), env.clone(), info.clone(), Uint64::new(2)).unwrap_err(); + println!("{:?}", res); + + //Schedule() sender "prop1" + let res = execute_schedule( + deps.as_mut(), + env.clone(), + info.clone(), + "target".to_string(), + data.clone(), + title.clone(), + description.clone(), + Scheduled::AtTime(Timestamp::from_seconds(140)), + Option::None, + ) + .unwrap(); + println!("{:?}", res); + + let info = mock_info("nobody", &[]); + //try Cancel() sender "nobody" operation_id "3" admin "creator" proposers "prop1, prop2" + let res = + execute_cancel(deps.as_mut(), env.clone(), info.clone(), Uint64::new(3)).unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + } + + #[test] + fn test_add_remove_proposer() { + let mut deps = mock_dependencies(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100); + let msg = InstantiateMsg { + admins: Option::None, + proposers: vec![], + min_delay: Duration::Time(10), + }; + let info = mock_info("creator", &[]); + + // instantiate + let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + println!("{:?}", res); + + //try remove_proposer sender "creator" proposer_address "prop1" proposers "" + let res = execute_remove_proposer( + deps.as_mut(), + env.clone(), + info.clone(), + "prop1".to_string(), + ) + .unwrap_err(); + assert_eq!( + res, + ContractError::NotFound { + address: "prop1".to_string() + } + ); + + let info = mock_info("no_admin", &[]); + //try remove_proposer sender "no_admin" proposer_address "prop1" proposers "" + let res = execute_remove_proposer( + deps.as_mut(), + env.clone(), + info.clone(), + "prop1".to_string(), + ) + .unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + //try add_proposer sender "no_admin" proposer_address "prop1" proposers "" + let res = execute_add_proposer( + deps.as_mut(), + env.clone(), + info.clone(), + "prop1".to_string(), + ) + .unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + + let info = mock_info("creator", &[]); + //add_proposer sender "creator" proposer_address "prop1" proposers "" + let res = execute_add_proposer( + deps.as_mut(), + env.clone(), + info.clone(), + "prop1".to_string(), + ) + .unwrap(); + println!("{:?}", res); + + //remove_proposer sender "no_admin" proposer_address "prop1" proposers "prop1" + let res = execute_remove_proposer( + deps.as_mut(), + env.clone(), + info.clone(), + "prop1".to_string(), + ) + .unwrap(); + println!("{:?}", res); + } + + #[test] + fn test_update_min_delay() { + let mut deps = mock_dependencies(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100); + let msg = InstantiateMsg { + admins: Option::None, + proposers: vec![], + min_delay: Duration::Time(10), + }; + let info = mock_info("creator", &[]); + + // instantiate + let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + println!("{:?}", res); + + //update_min_delay() sender "creator" + let res = execute_update_min_delay( + deps.as_mut(), + env.clone(), + info.clone(), + Duration::Time(100), + ) + .unwrap(); + println!("{:?}", res); + + let info = mock_info("no_admin", &[]); + //try update_min_delay() sender "no_admin" + let res = execute_update_min_delay( + deps.as_mut(), + env.clone(), + info.clone(), + Duration::Time(100), + ) + .unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + } + + #[test] + fn test_revoke_admin() { + let mut deps = mock_dependencies(); + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(100); + let msg = InstantiateMsg { + admins: Option::None, + proposers: vec![], + min_delay: Duration::Time(10), + }; + let info = mock_info("creator", &[]); + + // instantiate + let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg).unwrap(); + println!("{:?}", res); + + //try revoke_admin() sender "creator" admin_address "not_in_it" admin "creator" + let res = execute_revoke_admin( + deps.as_mut(), + env.clone(), + info.clone(), + "not_in_it".to_string(), + ) + .unwrap_err(); + assert_eq!( + res, + ContractError::NotFound { + address: "not_in_it".to_string() + } + ); + + //revoke_admin() sender "creator" admin_address "creator" admin "creator" + let res = execute_revoke_admin( + deps.as_mut(), + env.clone(), + info.clone(), + "creator".to_string(), + ) + .unwrap(); + println!("{:?}", res); + + //try revoke_admin() sender "creator" admin_address "creator" admin "" + let res = execute_revoke_admin( + deps.as_mut(), + env.clone(), + info.clone(), + "creator".to_string(), + ) + .unwrap_err(); + assert_eq!(res, ContractError::Unauthorized {}); + } +} + +``` +:: + +### msg.rs +::highlight-card +```rust +use crate::state::{Operation, OperationStatus}; +use cosmwasm_std::{Addr, Binary, Uint64}; +use cw_utils::{Duration, Scheduled}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InstantiateMsg { + pub admins: Option>, + pub proposers: Vec, + pub min_delay: Duration, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + Schedule { + target_address: String, + data: Binary, + title: String, + description: String, + execution_time: Scheduled, + executors: Option>, + }, + + Cancel { + operation_id: Uint64, + }, + + Execute { + operation_id: Uint64, + }, + + RevokeAdmin { + admin_address: String, + }, + + AddProposer { + proposer_address: String, + }, + + RemoveProposer { + proposer_address: String, + }, + + UpdateMinDelay { + new_delay: Duration, + }, + Freeze {}, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + GetOperationStatus { + operation_id: Uint64, + }, + + GetExecutionTime { + operation_id: Uint64, + }, + + GetAdmins {}, + + GetOperations { + start_after: Option, + limit: Option, + }, + + GetMinDelay {}, + + GetProposers {}, + + GetExecutors { + operation_id: Uint64, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct OperationResponse { + pub id: Uint64, + pub status: OperationStatus, + pub proposer: Addr, + pub executors: Option>, + pub execution_time: Scheduled, + pub target: Addr, + pub data: Binary, + pub title: String, + pub description: String, +} + +//impl Into for Operation changed to from due to lint warning +impl From for OperationResponse { + fn from(operation: Operation) -> OperationResponse { + OperationResponse { + id: operation.id, + status: operation.status, + proposer: operation.proposer, + executors: operation.executors, + execution_time: operation.execution_time, + target: operation.target, + data: operation.data, + title: operation.title, + description: operation.description, + } + } +} + +// We define a custom struct for each query response +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[allow(non_snake_case)] +pub struct OperationListResponse { + pub operationList: Vec, +} + +``` +:: + + +### error.rs +::highlight-card +```rust +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Delay time not ended")] + Unexpired {}, + + #[error("Address {address:?} not found in Proposers")] + NotFound { address: String }, + + #[error("Executed operations can not be cancelled.")] + NotDeletable {}, + + #[error("Proposers list already contains this proposer address")] + AlreadyContainsProposerAddress {}, + + #[error("Minimum Delay condition not satisfied.")] + MinDelayNotSatisfied {}, + + #[error("This operation already executed.")] + Executed {}, + + #[error("Changes can not be made on a frozen Timelock contract.")] + TimelockFrozen {}, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. +} + +``` +:: + +### state.rs +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{Addr, Binary, Uint64}; +use cw_storage_plus::{Item, Map}; +use cw_utils::{Duration, Scheduled}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Timelock { + pub admins: Vec, + pub proposers: Vec, + pub min_time_delay: Duration, + pub frozen: bool, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Operation { + pub id: Uint64, + pub status: OperationStatus, + pub proposer: Addr, + pub executors: Option>, + pub execution_time: Scheduled, + pub target: Addr, + pub data: Binary, + pub title: String, + pub description: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub enum OperationStatus { + Pending, + Ready, + Done, +} + +pub const CONFIG: Item = Item::new("timelock"); +pub const OPERATION_LIST: Map = Map::new("operation_list"); +pub const OPERATION_SEQ: Item = Item::new("operation_seq"); + +``` +:: + + +--------- +*Credits*: CosmWasm by example. +You can check the code on Github or open it with VS code. + + diff --git a/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/4.crowdfunding.md b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/4.crowdfunding.md new file mode 100644 index 00000000..600bbb3f --- /dev/null +++ b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/4.crowdfunding.md @@ -0,0 +1,804 @@ +--- +objectID: cosmwasm_by_example|crowdfunding +title: Crowdfunding +description: Learn how to create a crowdfunding contract. +parentSection: Developers +parentSectionPath: /developers +--- + +# 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 + +::highlight-card +```rust +pub mod contract; +mod error; +pub mod helpers; +pub mod msg; +pub mod rules; +pub mod state; +pub use crate::error::ContractError; +``` +:: + +### contract.rs + +::highlight-card +```rust +#[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 { + 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 { + 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 { + 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 { + 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 { + 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::, _>>()?; + let mut next_shares = total_shares; + let msgs: Vec = vec![]; + for (addr, shares) in user_shares { + let refund_amount = contract_balance.multiply_ratio(shares, total_shares); + let _bank_transfer_msg = CosmosMsg::::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 { + 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::::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 { + let output: StdResult = 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 { + 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 { + 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, +) -> StdResult { + let start = start_after + .map(|s| deps.api.addr_validate(&s)) + .transpose()? + .map(|addr| Bound::InclusiveRaw::(addr.as_bytes().to_vec())); + let funders = SHARES + .range(deps.storage, start, None, Order::Ascending) + .take(limit.u128() as usize) + .collect::, _>>()? + .iter() + .map(|(addr, shares)| (addr.to_string(), *shares)) + .collect::>(); + Ok(QueryResponseWrapper::GetFundersResponse( + GetFundersResponse { funders }, + )) +} + +pub fn get_funds(deps: Deps, _env: Env) -> StdResult { + 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 + +::highlight-card +```rust +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, + pub deadline: Timestamp, + pub name: String, + pub description: String, + pub execute_msg: Option, +} + +#[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, + }, + // returns total fund held by contract. + GetTotalFunds {}, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] // returns config +pub 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 Response +pub 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 { + 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 + +::highlight-card +```rust +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 +::highlight-card +```rust +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 = Item::new("config"); +pub const SHARES: Map = Map::new("shares"); +pub const TOTAL_SHARES: Item = Item::new("total_shares"); +pub const EXECUTE_MSG: Item> = Item::new("execute_msg"); + + + +``` +:: + +### helpers.rs + +::highlight-card +```rust +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>(&self, msg: T) -> StdResult { +// let msg = to_binary(&msg.into())?; +// Ok(WasmMsg::Execute { +// contract_addr: self.addr().into(), +// msg, +// funds: vec![], +// } +// .into()) +// } + +// /// Get Custom +// pub fn custom_query(&self, querier: &Q, val: String) -> StdResult +// where +// Q: Querier, +// T: Into, +// 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::::new(querier).query(&query)?; +// Ok(res) +// } +// } + +``` +:: + +### rules.rs +::highlight-card +```rust +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. + + diff --git a/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/5.responses-attributes.md b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/5.responses-attributes.md new file mode 100644 index 00000000..d5470ec2 --- /dev/null +++ b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/5.responses-attributes.md @@ -0,0 +1,408 @@ +--- +objectID: cosmwasm_by_example|responses-attributes +title: Responses +description: Learn how to manage responses with CosmWasm. +parentSection: Developers +parentSectionPath: /developers +--- + +# Responses and Attributes in Cosmwasm + + +## Explanation + +### Responses +A response is the final point of a Cosmwasm endpoint (e.g. instantiation and execution), and it has several uses. + +It is responsible of returning the metadata when a certain endpoint is called. A response holds attributes which are key-value pairs that are attached when a message is executed that can be queried. +It triggers events that can be then queried on a Tendermint or low-level. +It can hold messages that the contract should execute when the function is completed. +It can return data to the user, which is important in queries. + + +### Attributes +Attributes are metadata that are used to log information about a certain endpoint to help with filtering and indexing data. The method attribute is normally added to represent an instantiation has been executed on the contract and can be used to filter the instantiation message. +::highlight-card +```rust +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let state = State { + count: msg.count, + owner: info.sender.clone(), + }; + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + STATE.save(deps.storage, &state)?; + + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("owner", info.sender) + .add_attribute("count", msg.count.to_string())) +} +``` +:: +### Messages +Responses can hold messages that the contract executes after all the code inside a function is successfully completed. These messages are synchronous and the message is part of the transaction itself. If the transaction or message fails to execute, then the whole execution fails and reverts. Similarly to the Send Tokens example, the message could be any CosmosMsg which includes Bank messages (sending native funds), Staking messages or Wasm messages (instantiating and executing). +::highlight-card +```rust +Ok(Response::new().add_attribute("action", "increment") + .add_message(BankMsg::Send { to_address: to.into_string(), amount: vec![Coin{denom, amount}] })) +This message in the response is executed by the smart contract after the logic of the function is completed. +``` +:: +### SubMessages +A Submessage can be used instead of a message when a reply is expected as a callback from calling a certain function on another contract or any other message. A submessage is asynchronous meaning that even if the message fails on the recipient contract, the whole transaction does not fail but a reply could be sent to handle the error respectively. This is extremely important in IBC use cases where if the packet is rejected, we should not want the whole transaction to fail but only to get a reply of an aknowledgment of failure from the other contract. +::highlight-card +```rust +pub fn ibc_packet_receive( + deps: DepsMut, + _env: Env, + msg: IbcPacketReceiveMsg, +) -> Result { + // other parse code here... + + let msg = Cw20ExecuteMsg::Transfer { + recipient, + amount: coin.amount, + }; + let exec = WasmMsg::Execute { + contract_addr: coin.address, + msg: to_binary(&msg).unwrap(), + funds: vec![], + }; + let sub_msg = SubMsg::reply_on_error(exec, SEND_TOKEN_ID); + + // return submessage with pre-emptive success packet in the data field + IbcReceiveResponse::new() + .set_ack(ack_success()) + .add_submessage(sub_msg) + .add_attributes(attributes) +} +``` +:: + + +## Example + +To handle responses with CosmWasm, you can create the following files: +lib.rs +contract.rs +msg.rs +error.rs +state.rs +helpers.rs + + +### lib.rs +::highlight-card +```rust +pub mod contract; +mod error; +pub mod helpers; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; + +``` +:: + +### contract.rs +::highlight-card +```rust +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cw2::set_contract_version; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, GetCountResponse, InstantiateMsg, QueryMsg}; +use crate::state::{State, STATE}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:responses-attributes"; +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 { + let state = State { + count: msg.count, + owner: info.sender.clone(), + }; + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + STATE.save(deps.storage, &state)?; + + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("owner", info.sender) + .add_attribute("count", msg.count.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Increment {} => execute::increment(deps), + ExecuteMsg::Reset { count } => execute::reset(deps, info, count), + } +} + +pub mod execute { + use super::*; + + pub fn increment(deps: DepsMut) -> Result { + STATE.update(deps.storage, |mut state| -> Result<_, ContractError> { + state.count += 1; + Ok(state) + })?; + + Ok(Response::new().add_attribute("action", "increment")) + } + + pub fn reset(deps: DepsMut, info: MessageInfo, count: i32) -> Result { + STATE.update(deps.storage, |mut state| -> Result<_, ContractError> { + if info.sender != state.owner { + return Err(ContractError::Unauthorized {}); + } + state.count = count; + Ok(state) + })?; + Ok(Response::new().add_attribute("action", "reset")) + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GetCount {} => to_binary(&query::count(deps)?), + } +} + +pub mod query { + use super::*; + + pub fn count(deps: Deps) -> StdResult { + let state = STATE.load(deps.storage)?; + Ok(GetCountResponse { count: state.count }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::{coins, from_binary}; + + #[test] + fn proper_initialization() { + let mut deps = mock_dependencies(); + + let msg = InstantiateMsg { count: 17 }; + let info = mock_info("creator", &coins(1000, "earth")); + + // we can just call .unwrap() to assert this was a success + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // it worked, let's query the state + let res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap(); + let value: GetCountResponse = from_binary(&res).unwrap(); + assert_eq!(17, value.count); + } + + #[test] + fn increment() { + let mut deps = mock_dependencies(); + + let msg = InstantiateMsg { count: 17 }; + let info = mock_info("creator", &coins(2, "token")); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // beneficiary can release it + let info = mock_info("anyone", &coins(2, "token")); + let msg = ExecuteMsg::Increment {}; + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // should increase counter by 1 + let res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap(); + let value: GetCountResponse = from_binary(&res).unwrap(); + assert_eq!(18, value.count); + } + + #[test] + fn reset() { + let mut deps = mock_dependencies(); + + let msg = InstantiateMsg { count: 17 }; + let info = mock_info("creator", &coins(2, "token")); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // beneficiary can release it + let unauth_info = mock_info("anyone", &coins(2, "token")); + let msg = ExecuteMsg::Reset { count: 5 }; + let res = execute(deps.as_mut(), mock_env(), unauth_info, msg); + match res { + Err(ContractError::Unauthorized {}) => {} + _ => panic!("Must return unauthorized error"), + } + + // only the original creator can reset the counter + let auth_info = mock_info("creator", &coins(2, "token")); + let msg = ExecuteMsg::Reset { count: 5 }; + let _res = execute(deps.as_mut(), mock_env(), auth_info, msg).unwrap(); + + // should now be 5 + let res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap(); + let value: GetCountResponse = from_binary(&res).unwrap(); + assert_eq!(5, value.count); + } +} + +``` +:: + +### msg.rs +::highlight-card +```rust +use cosmwasm_schema::{cw_serde, QueryResponses}; + +#[cw_serde] +pub struct InstantiateMsg { + pub count: i32, +} + +#[cw_serde] +pub enum ExecuteMsg { + Increment {}, + Reset { count: i32 }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + // GetCount returns the current count as a json-encoded number + #[returns(GetCountResponse)] + GetCount {}, +} + +// We define a custom struct for each query response +#[cw_serde] +pub struct GetCountResponse { + pub count: i32, +} + +``` +:: + + +### error.rs +::highlight-card +```rust +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. +} + +``` +:: + +### state.rs +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct State { + pub count: i32, + pub owner: Addr, +} + +pub const STATE: Item = Item::new("state"); + +``` +:: + +### helpers.rs +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{ + to_binary, Addr, CosmosMsg, CustomQuery, Querier, QuerierWrapper, StdResult, WasmMsg, WasmQuery, +}; + +use crate::msg::{ExecuteMsg, GetCountResponse, QueryMsg}; + +/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers +/// for working with this. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct CwTemplateContract(pub Addr); + +impl CwTemplateContract { + pub fn addr(&self) -> Addr { + self.0.clone() + } + + pub fn call>(&self, msg: T) -> StdResult { + let msg = to_binary(&msg.into())?; + Ok(WasmMsg::Execute { + contract_addr: self.addr().into(), + msg, + funds: vec![], + } + .into()) + } + + /// Get Count + pub fn count(&self, querier: &Q) -> StdResult + where + Q: Querier, + T: Into, + CQ: CustomQuery, + { + let msg = QueryMsg::GetCount {}; + let query = WasmQuery::Smart { + contract_addr: self.addr().into(), + msg: to_binary(&msg)?, + } + .into(); + let res: GetCountResponse = QuerierWrapper::::new(querier).query(&query)?; + Ok(res) + } +} + +``` +:: + +--------- +*Credits*: CosmWasm by example. +You can check the code on Github or open it with VS code. + + diff --git a/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/6.read-write-sate.md b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/6.read-write-sate.md new file mode 100644 index 00000000..9772ea30 --- /dev/null +++ b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/6.read-write-sate.md @@ -0,0 +1,398 @@ +--- +objectID: cosmwasm_by_example|read-write-state +title: Read and Write in a contract +description: Learn how to read and write in smart contracts' state. +parentSection: Developers +parentSectionPath: /developers +--- +# Read and Write +## Explanation + +Smart contract that explains the basics of reading and writing to the state in a smart contract. The state of the smart contract is the allocated storage for the smart contract in the blockchain. The state is definted usually in a separate file state.rs that imports and uses Item and Map and other types from **cw_storage_plus**. + +### State +::highlight-card +```rust +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct State { + pub count: i32, + pub owner: Addr, +} + +pub const STATE: Item = Item::new("state"); + +// Map is another type of storage that stores key-value pairs in storage. +pub const NAMES: Map = Map::new("names"); +``` +:: +Each constant variable in state is stored as a key value pair in the blockchain and therefor can be read or altered. + +### Writing to State +::highlight-card +```rust +// contract.rs + +pub fn write(deps: DepsMut) -> Result { + /* This execute endpoint writes a new owner to state. */ + + // First we need to load the current state from the blockchain from `deps.storage` as mutable. + let mut state = STATE.load(deps.storage)?; + state.count = 5; + + // Save the new state with the changed variables in storage. + STATE.save(deps.storage, &state)?; + + // Now let us add a new key-value pair to the `NAMES` map in storage. + NAMES.save(deps.storage, "Georges".to_string(), &"Chouchani".to_string())?; + + Ok(Response::new().add_attribute("action", "write")) + } +Structs can be stored as an Item in storage and key-value pairs are stored as Map. + +Reading from State +Reading from Structs or constants +// contract.rs +pub fn count(deps: Deps) -> StdResult { + // Loads the state from storage and checks the count variable. + let state = STATE.load(deps.storage)?; + Ok(GetCountResponse { count: state.count }) + } + +``` +:: +The constant is loaded from storage and variables inside the struct (if it is one) can be directly accessed. + +### Reading Maps +Maps are usually read by supplying a key and checking if a value exists. Keys and values can also be iterated through. +::highlight-card +```rust +pub fn name(deps: Deps, first_name: String) -> StdResult { + // Loads the NAMES Map from storage for the key `first_name` to get the `last_name` + // `may_load` returns None if the key does not exist in the map. `load` returns an error. + let res = NAMES.may_load(deps.storage, first_name)?; + Ok(GetNameResponse{family_name: res.unwrap()}) + } + +``` +:: + + +## Example + +To read and write in state with CosmWasm, you can create the following files: +lib.rs +contract.rs +msg.rs +error.rs +state.rs +helpers.rs + +### lib.rs + +::highlight-card +```rust +pub mod contract; +mod error; +pub mod helpers; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; +``` +:: + +### contract.rs +::highlight-card +```rust +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cw2::set_contract_version; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, GetCountResponse, InstantiateMsg, QueryMsg}; +use crate::state::{State, STATE, NAMES}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:read-write-state"; +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 { + let state = State { + count: msg.count, + owner: info.sender.clone(), + }; + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + STATE.save(deps.storage, &state)?; + + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("owner", info.sender) + .add_attribute("count", msg.count.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Write {} => execute::write(deps), + } +} + +pub mod execute { + use super::*; + + pub fn write(deps: DepsMut) -> Result { + /* This execute endpoint writes a new owner to state. */ + + // First we need to load the current state from the blockchain from `deps.storage` as mutable. + let mut state = STATE.load(deps.storage)?; + state.count = 5; + + // Save the new state with the changed variables in storage. + STATE.save(deps.storage, &state)?; + + // Now let us add a new key-value pair to the `NAMES` map in storage. + NAMES.save(deps.storage, "Georges".to_string(), &"Chouchani".to_string())?; + + Ok(Response::new().add_attribute("action", "write")) + } + +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GetCount {} => to_binary(&query::count(deps)?), + QueryMsg::GetFamilyName {first_name} => to_binary(&query::name(deps, first_name)?), + + } +} + +pub mod query { + use crate::msg::GetNameResponse; + + use super::*; + + pub fn count(deps: Deps) -> StdResult { + // Loads the state from storage and checks the count variable. + let state = STATE.load(deps.storage)?; + Ok(GetCountResponse { count: state.count }) + } + + pub fn name(deps: Deps, first_name: String) -> StdResult { + // Loads the NAMES Map from storage for the key `first_name` to get the `last_name` + // `may_load` returns None if the key does not exist in the map. `load` returns an error. + let res = NAMES.may_load(deps.storage, first_name)?; + Ok(GetNameResponse{family_name: res.unwrap()}) + } +} + +#[cfg(test)] +mod tests { + use crate::msg::GetNameResponse; + + use super::*; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::{coins, from_binary}; + + #[test] + fn proper_initialization() { + let mut deps = mock_dependencies(); + + let msg = InstantiateMsg { count: 17 }; + let info = mock_info("creator", &coins(1000, "earth")); + + // we can just call .unwrap() to assert this was a success + let res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_eq!(0, res.messages.len()); + + // it worked, let's query the state + let res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap(); + let value: GetCountResponse = from_binary(&res).unwrap(); + assert_eq!(17, value.count); + } + + #[test] + fn write() { + let mut deps = mock_dependencies(); + + let msg = InstantiateMsg { count: 17 }; + let info = mock_info("creator", &coins(2, "token")); + let _res = instantiate(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // Execute the function that changes the state of the blockchain. + let info = mock_info("anyone", &coins(2, "token")); + let msg = ExecuteMsg::Write {}; + let _res = execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + + // The counter should now be 5 + let res = query(deps.as_ref(), mock_env(), QueryMsg::GetCount {}).unwrap(); + let value: GetCountResponse = from_binary(&res).unwrap(); + + assert_eq!(5, value.count); + + // Last name of Georges can now be found. + let res = query(deps.as_ref(), mock_env(), QueryMsg::GetFamilyName { first_name: "Georges".to_string() } ).unwrap(); + let value: GetNameResponse = from_binary(&res).unwrap(); + + assert_eq!(value.family_name, "Chouchani".to_string()) + } + +} +``` +:: + +### msg.rs +::highlight-card +```rust +use cosmwasm_schema::{cw_serde, QueryResponses}; + +#[cw_serde] +pub struct InstantiateMsg { + pub count: i32, +} + +#[cw_serde] +pub enum ExecuteMsg { + Write {}, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + // GetCount returns the current count as a json-encoded number + #[returns(GetCountResponse)] + GetCount {}, + + #[returns(GetNameResponse)] + GetFamilyName { + first_name: String, + } +} + +#[cw_serde] +pub struct GetCountResponse { + pub count: i32, +} + +#[cw_serde] +pub struct GetNameResponse { + pub family_name: String, +} + + +``` +:: + +### error.rs +::highlight-card +```rust +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. +} + +``` +:: + +### state.rs +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::Addr; +use cw_storage_plus::{Item, Map}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct State { + pub count: i32, + pub owner: Addr, +} + +pub const STATE: Item = Item::new("state"); + +// Map is another type of storage that stores key-value pairs in storage. +pub const NAMES: Map = Map::new("names"); + +``` +:: + +### helpers.rs +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{ + to_binary, Addr, CosmosMsg, CustomQuery, Querier, QuerierWrapper, StdResult, WasmMsg, WasmQuery, +}; + +use crate::msg::{ExecuteMsg, GetCountResponse, QueryMsg}; + +/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers +/// for working with this. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct CwTemplateContract(pub Addr); + +impl CwTemplateContract { + pub fn addr(&self) -> Addr { + self.0.clone() + } + + pub fn call>(&self, msg: T) -> StdResult { + let msg = to_binary(&msg.into())?; + Ok(WasmMsg::Execute { + contract_addr: self.addr().into(), + msg, + funds: vec![], + } + .into()) + } + + /// Get Count + pub fn count(&self, querier: &Q) -> StdResult + where + Q: Querier, + T: Into, + CQ: CustomQuery, + { + let msg = QueryMsg::GetCount {}; + let query = WasmQuery::Smart { + contract_addr: self.addr().into(), + msg: to_binary(&msg)?, + } + .into(); + let res: GetCountResponse = QuerierWrapper::::new(querier).query(&query)?; + Ok(res) + } +} + +``` +:: + +--------- +*Credits*: CosmWasm by example. +You can check the code on Github or open it with VS code. + + diff --git a/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/7.sending-tokens.md b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/7.sending-tokens.md new file mode 100644 index 00000000..427d943f --- /dev/null +++ b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/7.sending-tokens.md @@ -0,0 +1,356 @@ +--- +objectID: cosmwasm_by_example|sending-tokens +title: Sending Tokens +description: Learn how to send tokens with CosmWasm. +parentSection: Developers +parentSectionPath: /developers +--- + +# Sending Tokens +This section shows a smart contract designed to send a blockchain's native tokens to a recipient specified by the original sender in the execute message. + +## Explanation + +### send_tokens +Once the main use case of this function is executed (which in this context is void), a bank message is appended for the contract to act upon. It's worth noting that the contract becomes the signer of the transaction, not the initiating sender. +::highlight-card +```rust +// contract.rs + +pub fn send_tokens( + _deps: DepsMut, + amount: Uint128, + denom: String, + to: Addr +) -> Result { + + /* Sending tokens is managed via the response of this function. + A developer crafts a BankMsg to transmit tokens to a specified address using the native token. + The function will fail if the smart contract lacks sufficient tokens. + If any error surfaces prior to the response's generation, funds won't be transmitted. */ + + Ok(Response::new() + .add_attribute("action", "send") + .add_message(BankMsg::Send { + to_address: to.into_string(), + amount: vec![Coin{denom, amount}] + }) + ) +} +``` +:: +### Integration Testing +::highlight-card +```rust +// integration_tests.rs + +fn balance() { + let (mut app, cw_template_contract) = proper_instantiate(); + + let msg = ExecuteMsg::SendTokens { + amount: Uint128::new(10), + denom: "token".to_string(), + to: Addr::unchecked("receiver") + }; + + let funds_sent = Coin::new(10u128, "token".to_string()); + let cosmos_msg = cw_template_contract.call(msg, funds_sent).unwrap(); + app.execute(Addr::unchecked(USER), cosmos_msg).unwrap(); + + let balance = app.wrap().query_balance("receiver", "token").unwrap(); + assert_eq!(balance.amount, Uint128::new(10)); + assert_eq!(balance.denom, "token"); +} +``` +:: + +## Example + +To send tokens with CosmWasm, you can create the following files: +lib.rs +contract.rs +msg.rs +error.rs +state.rs +helpers.rs +integration_tests.rs + + +### lib.rs + +::highlight-card +```rust +pub mod contract; +mod error; +pub mod msg; +pub mod state; +pub mod integration_tests; +pub mod helpers; + +pub use crate::error::ContractError; + +``` +:: + +### contract.rs +::highlight-card +```rust +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + + + + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result { + + Ok(Response::new() + .add_attribute("method", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::SendTokens {amount, denom, to} => execute::send_tokens(deps, amount, denom, to), + } +} + +pub mod execute { + use cosmwasm_std::{Uint128, Addr, BankMsg, Coin}; + + use super::*; + + pub fn send_tokens(_deps: DepsMut, amount: Uint128, denom: String, to: Addr) -> Result { + + Ok(Response::new().add_attribute("action", "increment") + /* Sending tokens is part of the response of a function + Developer creates a BankMsg to send tokens to an address with a specific native token + Will fail if smart contract does not have this much tokens initially */ + .add_message(BankMsg::Send { to_address: to.into_string(), amount: vec![Coin{denom, amount}] })) + } + + +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + } +} + +pub mod query { + + +} + +#[cfg(test)] +mod tests { +} +``` +:: + +### msg.rs +::highlight-card +```rust +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Uint128, Addr, Coin}; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + SendTokens {amount: Uint128, denom: String, to: Addr} +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + +} + +#[cw_serde] +pub struct BalanceResponse { + pub amount: Coin, +} + +``` +:: + +### error.rs +::highlight-card +```rust +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. +} + +``` +:: + +### state.rs +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct State { + pub count: i32, + pub owner: Addr, +} + +pub const STATE: Item = Item::new("state"); + +``` +:: + +### helpers.rs +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{ + to_binary, Addr, CosmosMsg, StdResult, WasmMsg, Coin, +}; + +use crate::msg::{ExecuteMsg}; + +/// CwTemplateContract is a wrapper around Addr that provides a lot of helpers +/// for working with this. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct CwTemplateContract(pub Addr); + +impl CwTemplateContract { + pub fn addr(&self) -> Addr { + self.0.clone() + } + + pub fn call>(&self, msg: T, funds: Coin) -> StdResult { + let msg = to_binary(&msg.into())?; + Ok(WasmMsg::Execute { + contract_addr: self.addr().into(), + msg, + funds: vec![funds], + } + .into()) + } + + +} +``` +:: + +### integration_tests.rs +::highlight-card +```rust +#[cfg(test)] +mod tests { + use crate::helpers::CwTemplateContract; + use crate::msg::InstantiateMsg; + use cosmwasm_std::{Addr, Coin, Empty, Uint128}; + use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper, Executor}; + pub fn contract_template() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + Box::new(contract) + } + + const USER: &str = "USER"; + const ADMIN: &str = "ADMIN"; + const NATIVE_DENOM: &str = "token"; + + fn mock_app() -> App { + AppBuilder::new().build(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(USER), + vec![Coin { + denom: NATIVE_DENOM.to_string(), + amount: Uint128::new(10), + }], + ) + .unwrap(); + }) + } + + fn proper_instantiate() -> (App, CwTemplateContract) { + let mut app = mock_app(); + let cw_template_id = app.store_code(contract_template()); + + let msg = InstantiateMsg {}; + let cw_template_contract_addr = app + .instantiate_contract( + cw_template_id, + Addr::unchecked(ADMIN), + &msg, + &[], + "test", + None, + ) + .unwrap(); + + let cw_template_contract = CwTemplateContract(cw_template_contract_addr); + + (app, cw_template_contract) + } + + mod count { + use super::*; + use crate::msg::ExecuteMsg; + + #[test] + fn balance() { + let (mut app, cw_template_contract) = proper_instantiate(); + + let msg = ExecuteMsg::SendTokens { amount: Uint128::new(10), denom: "token".to_string(), to: Addr::unchecked("receiver") } ; + let funds_sent = Coin::new(10u128, "token".to_string()); + let cosmos_msg = cw_template_contract.call(msg, funds_sent).unwrap(); + app.execute(Addr::unchecked(USER), cosmos_msg).unwrap(); + let balance = app.wrap().query_balance("receiver", "token").unwrap(); + assert_eq!(balance.amount, Uint128::new(10)); + assert_eq!(balance.denom, "token"); + + } + } +} +``` +:: + +--------- +*Credits*: CosmWasm by example. +You can check the code on Github or open it with VS code. + + diff --git a/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/8.token-vault.md b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/8.token-vault.md new file mode 100644 index 00000000..9f0fb256 --- /dev/null +++ b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/8.token-vault.md @@ -0,0 +1,465 @@ +--- +objectID: cosmwasm_by_example|token_vault +title: Token Vaults +description: Learn how to create vaults with CosmWasm. +parentSection: Developers +parentSectionPath: /developers +--- + +# Token Vaults +## Explanation + +The Token Vault contract allows users to deposit and withdraw tokens. It uses the *cw20* token standard and maintains a mapping of user balances. + +The Vault contract is deployed with a specific *cw20* token address, which is passed as a parameter to the constructor. It provides the following functionalities: + +*deposit*: Allows users to deposit tokens into the vault. The number of shares to be minted for the user is calculated based on the deposited amount and the existing token balance of the vault. + +*withdraw*: Allows users to withdraw tokens from the vault. The number of tokens to be withdrawn is calculated based on the number of shares the user wants to burn and the current token balance of the vault. + +### Deposit +This function allows users to deposit tokens into the vault: + +::highlight-card +```rust +pub fn execute_deposit( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, + ) -> Result { + let token_info=TOKEN_INFO.load(deps.storage)?; + let mut total_supply=TOTAL_SUPPLY.load(deps.storage)?; + let mut shares=Uint128::zero(); + let mut balance=BALANCE_OF.load(deps.storage, info.sender.clone()).unwrap_or(Uint128::zero()); + let balance_of=get_token_balance_of(&deps, info.sender.clone(), token_info.token_address.clone())?; + if total_supply.is_zero() { + shares+=amount; + } + else { + shares+=amount.checked_mul(total_supply).map_err(StdError::overflow)?.checked_div(balance_of).map_err(StdError::divide_by_zero)? + } + + // Giving allowance to this contract + give_allowance(env.clone(), info.clone(), amount, token_info.token_address.clone())?; + total_supply+=shares; + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + balance+=shares; + BALANCE_OF.save(deps.storage, info.sender.clone(), &balance)?; + + let transfer_from_msg=cw20::Cw20ExecuteMsg::TransferFrom { owner: info.sender.to_string(), recipient: env.contract.address.to_string(), amount }; + let msg=CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { contract_addr: token_info.token_address.to_string(), msg: to_binary(&transfer_from_msg)?, funds: info.funds }); + + Ok(Response::new().add_attribute("action", "deposit").add_message(msg)) + + } + +``` +:: + + +### Withdraw +This function allows users to withdraw tokens from the vault: + +::highlight-card +```rust + pub fn execute_withdraw( + deps: DepsMut, + _env: Env, + info: MessageInfo, + shares: Uint128, + ) -> Result { + let token_info=TOKEN_INFO.load(deps.storage)?; + let mut total_supply=TOTAL_SUPPLY.load(deps.storage)?; + let mut balance=BALANCE_OF.load(deps.storage, info.sender.clone()).unwrap_or(Uint128::zero()); + let balance_of=get_token_balance_of(&deps, info.sender.clone(), token_info.token_address.clone())?; + let amount=shares.checked_mul(balance_of).map_err(StdError::overflow)?.checked_div(total_supply).map_err(StdError::divide_by_zero)?; + total_supply-=shares; + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + balance-=shares; + BALANCE_OF.save(deps.storage, info.sender.clone(), &balance)?; + + let transfer_msg=cw20::Cw20ExecuteMsg::Transfer { recipient: info.sender.to_string(), amount}; + let msg=CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { contract_addr: token_info.token_address.to_string(), msg: to_binary(&transfer_msg)?, funds: info.funds }); + + Ok(Response::new().add_attribute("action", "withdraw").add_message(msg)) + } +``` +:: + +## Example +To create a token vault with CosmWasm, you can create the following files: +lib.rs +contract.rs +msg.rs +error.rs +state.rs + +### lib.rs + +::highlight-card +```rust +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; +``` +:: + +### contract.rs +::highlight-card +```rust +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, StdError, Addr, Uint128, +}; +use cw2::set_contract_version; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::*; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:token-vault"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + /* Deps allows to access: + 1. Read/Write Storage Access + 2. General Blockchain APIs + 3. The Querier to the blototal_supplyckchain (raw data queries) */ + deps: DepsMut, + /* env gives access to global variables which represent environment information. + For exaample: + - Block Time/Height + - contract address + - Transaction Info */ + _env: Env, + /* Message Info gives access to information used for authorization. + 1. Funds sent with the message. + 2. The message sender (signer). */ + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + /* Instantiating the state that will be stored to the blockchain */ + let total_supply=Uint128::zero(); + let token_info=TokenInfo{ token_denom: msg.token_symbol, token_address: msg.token_contract_address }; + // Save the stete in deps.storage which creates a storage for contract data on the blockchain. + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + TOKEN_INFO.save(deps.storage, &token_info)?; + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION).unwrap(); + + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("total_supply", total_supply)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg {ExecuteMsg::Deposit{amount}=>execute::execute_deposit(deps,env,info,amount), + ExecuteMsg::Withdraw { shares } => execute::execute_withdraw(deps,env,info,shares), } +} +pub mod execute { + use cosmwasm_std::{CosmosMsg, WasmQuery}; + + use super::*; + + pub fn execute_deposit( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, + ) -> Result { + let token_info=TOKEN_INFO.load(deps.storage)?; + let mut total_supply=TOTAL_SUPPLY.load(deps.storage)?; + let mut shares=Uint128::zero(); + let mut balance=BALANCE_OF.load(deps.storage, info.sender.clone()).unwrap_or(Uint128::zero()); + let balance_of=get_token_balance_of(&deps, info.sender.clone(), token_info.token_address.clone())?; + if total_supply.is_zero() { + shares+=amount; + } + else { + shares+=amount.checked_mul(total_supply).map_err(StdError::overflow)?.checked_div(balance_of).map_err(StdError::divide_by_zero)? + } + + give_allowance(env.clone(), info.clone(), amount, token_info.token_address.clone())?; + total_supply+=shares; + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + balance+=shares; + BALANCE_OF.save(deps.storage, info.sender.clone(), &balance)?; + + let transfer_from_msg=cw20::Cw20ExecuteMsg::TransferFrom { owner: info.sender.to_string(), recipient: env.contract.address.to_string(), amount }; + let msg=CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { contract_addr: token_info.token_address.to_string(), msg: to_binary(&transfer_from_msg)?, funds: info.funds }); + + Ok(Response::new().add_attribute("action", "deposit").add_message(msg)) + + + } + + pub fn execute_withdraw( + deps: DepsMut, + _env: Env, + info: MessageInfo, + shares: Uint128, + ) -> Result { + let token_info=TOKEN_INFO.load(deps.storage)?; + let mut total_supply=TOTAL_SUPPLY.load(deps.storage)?; + let mut balance=BALANCE_OF.load(deps.storage, info.sender.clone()).unwrap_or(Uint128::zero()); + let balance_of=get_token_balance_of(&deps, info.sender.clone(), token_info.token_address.clone())?; + let amount=shares.checked_mul(balance_of).map_err(StdError::overflow)?.checked_div(total_supply).map_err(StdError::divide_by_zero)?; + total_supply-=shares; + TOTAL_SUPPLY.save(deps.storage, &total_supply)?; + balance-=shares; + BALANCE_OF.save(deps.storage, info.sender.clone(), &balance)?; + + let transfer_msg=cw20::Cw20ExecuteMsg::Transfer { recipient: info.sender.to_string(), amount}; + let msg=CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { contract_addr: token_info.token_address.to_string(), msg: to_binary(&transfer_msg)?, funds: info.funds }); + + Ok(Response::new().add_attribute("action", "withdraw").add_message(msg)) + + + } + + + pub fn get_token_balance_of( + deps: &DepsMut, + user_address: Addr, + cw20_contract_addr: Addr, + ) -> Result { + let query_msg=cw20::Cw20QueryMsg::Balance { address: user_address.to_string() }; + let msg=deps.querier.query(&cosmwasm_std::QueryRequest::Wasm(WasmQuery::Smart { contract_addr: cw20_contract_addr.to_string(), msg: to_binary(&query_msg)? }))?; + + Ok(msg) + } + + pub fn give_allowance( + env: Env, + info: MessageInfo, + amount: Uint128, + cw20_contract_addr: Addr, + ) -> Result { + let allowance_msg=cw20::Cw20ExecuteMsg::IncreaseAllowance { spender: env.contract.address.to_string(), amount , expires: None }; + let msg=CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { contract_addr: cw20_contract_addr.to_string(), msg: to_binary(&allowance_msg)?, funds: info.funds }); + + Ok(Response::new().add_attribute("action", "give_Allowance").add_message(msg)) + } + +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + match msg {QueryMsg::GetTotalSupply{}=>query::get_total_supply(deps), + QueryMsg::GetBalanceOf { address } => query::get_balance_of(deps,address) } +} + +pub mod query { + + use super::*; + + pub fn get_total_supply(deps: Deps) -> Result { + let total_supply = TOTAL_SUPPLY.load(deps.storage)?; + + to_binary(&total_supply) + } + + pub fn get_balance_of(deps: Deps,addr: Addr) -> Result { + let balance_of = BALANCE_OF.load(deps.storage,addr)?; + + to_binary(&balance_of) + } + +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::{testing::{mock_dependencies, mock_env, mock_info}, coins, Uint128, Addr, StdError}; + + use crate::{msg::{InstantiateMsg, ExecuteMsg}, contract::{instantiate,execute,}, ContractError}; + + + + #[test] +fn test_instantiate() { + let mut deps = mock_dependencies(); + + let msg = InstantiateMsg { token_symbol: "ABC".to_string(), token_contract_address: Addr::unchecked("abcdef") }; + let info = mock_info("creator", &coins(1000, "earth")); + + // we can just call .unwrap() to assert this was a success + let res = instantiate(deps.as_mut(), mock_env(), info, msg); + assert!(res.is_ok()); + + // Assert the response contains the expected attributes + let response = res.unwrap(); + assert_eq!(response.attributes.len(), 2); + assert_eq!(response.attributes[0].key, "method"); + assert_eq!(response.attributes[0].value, "instantiate"); + assert_eq!(response.attributes[1].key, "total_supply"); + assert_eq!(response.attributes[1].value, Uint128::zero().to_string()); +} + +#[test] +fn test_execute_receive() { + let mut deps = mock_dependencies(); + let info = mock_info("sender", &[]); + + + let msg = InstantiateMsg { token_symbol: "ABC".to_string(), token_contract_address: Addr::unchecked("abcdef") }; + // we can just call .unwrap() to assert this was a success + let res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg); + assert!(res.is_ok()); + + // Assert the response contains the expected attributes + let response = res.unwrap(); + assert_eq!(response.attributes.len(), 2); + assert_eq!(response.attributes[0].key, "method"); + assert_eq!(response.attributes[0].value, "instantiate"); + assert_eq!(response.attributes[1].key, "total_supply"); + assert_eq!(response.attributes[1].value, Uint128::zero().to_string()); + + let msg=ExecuteMsg::Deposit { amount: Uint128::new(10) }; + let err=execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + + + assert_eq!(err, + ContractError::Std(StdError::GenericErr {msg: "Querier system error: No such contract: abcdef".to_string()})); + +} + +#[test] +fn test_execute_withdraw() { + let mut deps = mock_dependencies(); + let info = mock_info("sender", &[]); + + + let msg = InstantiateMsg { token_symbol: "ABC".to_string(), token_contract_address: Addr::unchecked("abcdef") }; + // we can just call .unwrap() to assert this was a success + let res = instantiate(deps.as_mut(), mock_env(), info.clone(), msg); + assert!(res.is_ok()); + + // Assert the response contains the expected attributes + let response = res.unwrap(); + assert_eq!(response.attributes.len(), 2); + assert_eq!(response.attributes[0].key, "method"); + assert_eq!(response.attributes[0].value, "instantiate"); + assert_eq!(response.attributes[1].key, "total_supply"); + assert_eq!(response.attributes[1].value, Uint128::zero().to_string()); + + let msg=ExecuteMsg::Withdraw { shares: Uint128::new(10) }; + let err=execute(deps.as_mut(), mock_env(), info, msg).unwrap_err(); + + + assert_eq!(err, + ContractError::Std(StdError::GenericErr {msg: "Querier system error: No such contract: abcdef".to_string()})); + +} +} + +``` +:: + + +### msg.rs +::highlight-card +```rust +#[cw_serde] +pub enum ExecuteMsg { + + Deposit { + amount : Uint128 + }, + Withdraw { + shares: Uint128 + } +} + + + +///~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +////// Query +///~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(Uint128)] + GetTotalSupply {}, + + #[returns(Uint128)] + GetBalanceOf { + address: Addr + } +} + + +``` +:: + +### error.rs +::highlight-card +```rust +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug,PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Address not whitelisted")] + NotWhitelisted {}, + + #[error("To Do Error")] + ToDo {}, +} + +``` +:: + +### state.rs +::highlight-card +```rust +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Item, Map}; + + + +// Total Supply +pub const TOTAL_SUPPLY: Item = Item::new("total_supply"); + +// Balance of +pub const BALANCE_OF: Map=Map::new("balance_of"); + + +#[cw_serde] +pub struct TokenInfo{ + pub token_denom: String, + pub token_address: Addr + +} + +pub const TOKEN_INFO: Item = Item::new("token_info"); + + +``` +:: + +--------- +*Credits*: CosmWasm by example. +You can check the code on Github or open it with VS code. + + diff --git a/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/9.amm-product.md b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/9.amm-product.md new file mode 100644 index 00000000..9a85136d --- /dev/null +++ b/content/2.developers/4.cosmwasm-documentation/6.tutorials/3.cosmwasm-by-example/9.amm-product.md @@ -0,0 +1,1767 @@ +--- +objectID: cosmwasm_by_example|product_amm +title: Constant Product AMM +description: Learn how to create a Constant Product AMM with CosmWasm. +parentSection: Developers +parentSectionPath: /developers +--- + +# Constant Product Automatic Market Maker (AMM) +## Explanation +This contract is an Constant Product Automated Market Maker (AMM) for CosmWasm. This contract allows you to swap tokens. Liquidity providers can add liquidity to the market and receive a certain fee on every transaction that is set on instantiation. The code also includes various error handling and validation checks to ensure the correctness and security of the operations. + +This type of AMM is based on the function **x*y=k**, which establishes a range of prices for two tokens according to the available liquidity of each token. When the supply of token X increases, the token supply of Y must decrease, and vice-versa, to maintain the constant product K. When plotted, the result is a hyperbola where liquidity is always available but at increasingly higher prices, which approach infinity at both ends. + +### Instantiation + + +The contract can be instantiated with the following messages +::highlight-card +```rust +{ + "token1_denom": {"cw20": ""}, + "token2_denom": {"cw20": ""}, +} +``` +:: +Token denom can be cw20 for cw20 tokens. cw20 tokens have a contract address. CW20_CODE_ID is the code id for a basic cw20 binary. + + +### Add Liquidity +Allows a user to add liquidity to the pool. +::highlight-card +```rust +pub fn execute_add_liquidity( + deps: DepsMut, + info: &MessageInfo, + env: Env, + min_liquidity: Uint128, + token1_amount: Uint128, + token2_amount: Uint128, + expiration: Option, +) -> Result { + check_expiration(&expiration, &env.block)?; + + let token1 = TOKEN1.load(deps.storage)?; + let token2 = TOKEN2.load(deps.storage)?; + + let mut token_supply = TOTAL_STORED.load(deps.storage)?; + let liquidity_amount = token1_amount+token2_amount; + token_supply+=liquidity_amount; + TOTAL_STORED.save(deps.storage, &token_supply)?; + + if liquidity_amount < min_liquidity { + return Err(ContractError::MinLiquidityError { + min_liquidity, + liquidity_available: liquidity_amount, + }); + } + + // Generate cw20 transfer messages if necessary + let mut transfer_msgs: Vec = vec![]; + if let Cw20(addr) = token1.denom { + transfer_msgs.push(get_cw20_transfer_from_msg( + &info.sender, + &env.contract.address, + &addr, + token1_amount, + )?) + } + if let Cw20(addr) = token2.denom.clone() { + transfer_msgs.push(get_cw20_transfer_from_msg( + &info.sender, + &env.contract.address, + &addr, + token2_amount, + )?) + } + + + TOKEN1.update(deps.storage, |mut token1| -> Result<_, ContractError> { + token1.reserve += token1_amount; + Ok(token1) + })?; + TOKEN2.update(deps.storage, |mut token2| -> Result<_, ContractError> { + token2.reserve += token2_amount; + Ok(token2) + })?; + + Ok(Response::new() + .add_messages(transfer_msgs) + .add_attributes(vec![ + attr("token1_amount", token1_amount), + attr("token2_amount", token2_amount), + attr("liquidity_received", liquidity_amount), + ])) +} + +``` +:: +Users can add liquidity to the AMM by calling the execute_add_liquidity function. This function takes the desired amounts of two tokens (*token1_amount* and *token2_amount*) and mints a corresponding amount of liquidity tokens. The liquidity tokens represent the user's share in the AMM's liquidity pool. The function also transfers the input tokens from the user to the contract. + +### Remove Liquidity +Allows a user to remove liquidity from the pool. +::highlight-card +```rust +pub fn execute_remove_liquidity( + deps: DepsMut, + info: MessageInfo, + env: Env, + amount: Uint128, + min_token1: Uint128, + min_token2: Uint128, + expiration: Option, +) -> Result { + check_expiration(&expiration, &env.block)?; + + + let total_token_supply = TOTAL_STORED.load(deps.storage)?; + let token1 = TOKEN1.load(deps.storage)?; + let token2 = TOKEN2.load(deps.storage)?; + + if amount > total_token_supply { + return Err(ContractError::InsufficientLiquidityError { + requested: amount, + available: total_token_supply, + }); + } + + let token1_amount = amount + .checked_mul(token1.reserve) + .map_err(StdError::overflow)? + .checked_div(total_token_supply) + .map_err(StdError::divide_by_zero)?; + if token1_amount < min_token1 { + return Err(ContractError::MinToken1Error { + requested: min_token1, + available: token1_amount, + }); + } + + let token2_amount = amount + .checked_mul(token2.reserve) + .map_err(StdError::overflow)? + .checked_div(total_token_supply) + .map_err(StdError::divide_by_zero)?; + if token2_amount < min_token2 { + return Err(ContractError::MinToken2Error { + requested: min_token2, + available: token2_amount, + }); + } + + TOKEN1.update(deps.storage, |mut token1| -> Result<_, ContractError> { + token1.reserve = token1 + .reserve + .checked_sub(token1_amount) + .map_err(StdError::overflow)?; + Ok(token1) + })?; + + TOKEN2.update(deps.storage, |mut token2| -> Result<_, ContractError> { + token2.reserve = token2 + .reserve + .checked_sub(token2_amount) + .map_err(StdError::overflow)?; + Ok(token2) + })?; + + let token1_transfer_msg = match token1.denom { + Denom::Cw20(addr) => get_cw20_transfer_to_msg(&info.sender, &addr, token1_amount)?, + Denom::Native(_denom) => {unimplemented!()}, + }; + let token2_transfer_msg = match token2.denom { + Denom::Cw20(addr) => get_cw20_transfer_to_msg(&info.sender, &addr, token2_amount)?, + Denom::Native(_denom) => {unimplemented!()}, + }; + + Ok(Response::new() + .add_messages(vec![ + token1_transfer_msg, + token2_transfer_msg, + ]) + .add_attributes(vec![ + attr("liquidity_burned", amount), + attr("token1_returned", token1_amount), + attr("token2_returned", token2_amount), + ])) +} +``` +:: +Liquidity providers can remove their liquidity by calling the execute_remove_liquidity function. They specify the amount of liquidity tokens (amount) they want to burn, and the function calculates the proportionate amounts of the underlying tokens (token1_amount and token2_amount). The function transfers the corresponding tokens to the user and decreases the token reserves accordingly. + +### Swap +Swap one asset for the other +::highlight-card +```rust +pub fn execute_swap( + deps: DepsMut, + info: &MessageInfo, + input_amount: Uint128, + _env: Env, + input_token_enum: TokenSelect, + recipient: String, + min_token: Uint128, + expiration: Option, +) -> Result { + check_expiration(&expiration, &_env.block)?; + + let input_token_item = match input_token_enum { + TokenSelect::Token1 => TOKEN1, + TokenSelect::Token2 => TOKEN2, + }; + let input_token = input_token_item.load(deps.storage)?; + let output_token_item = match input_token_enum { + TokenSelect::Token1 => TOKEN2, + TokenSelect::Token2 => TOKEN1, + }; + let output_token = output_token_item.load(deps.storage)?; + + + let fees = FEES.load(deps.storage)?; + let total_fee_percent = fees.lp_fee_percent + fees.protocol_fee_percent; + let token_bought = get_input_price( + input_amount, + input_token.reserve, + output_token.reserve, + total_fee_percent, + )?; + + if min_token > token_bought { + return Err(ContractError::SwapMinError { + min: min_token, + available: token_bought, + }); + } + // Calculate fees + let protocol_fee_amount = get_protocol_fee_amount(input_amount, fees.protocol_fee_percent)?; + let input_amount_minus_protocol_fee = input_amount - protocol_fee_amount; + + let mut msgs = match input_token.denom.clone() { + Denom::Cw20(addr) => vec![get_cw20_transfer_from_msg( + &info.sender, + &_env.contract.address, + &addr, + input_amount_minus_protocol_fee, + )?], + Denom::Native(_) => vec![], + }; + + // Send protocol fee to protocol fee recipient + if !protocol_fee_amount.is_zero() { + msgs.push(get_fee_transfer_msg( + &info.sender, + &fees.protocol_fee_recipient, + &input_token.denom, + protocol_fee_amount, + )?) + } + + let recipient = deps.api.addr_validate(&recipient)?; + // Create transfer to message + msgs.push(match output_token.denom { + Denom::Cw20(addr) => get_cw20_transfer_to_msg(&recipient, &addr, token_bought)?, + Denom::Native(_denom) => {unimplemented!()}, + }); + + input_token_item.update( + deps.storage, + |mut input_token| -> Result<_, ContractError> { + input_token.reserve = input_token + .reserve + .checked_add(input_amount_minus_protocol_fee) + .map_err(StdError::overflow)?; + Ok(input_token) + }, + )?; + + output_token_item.update( + deps.storage, + |mut output_token| -> Result<_, ContractError> { + output_token.reserve = output_token + .reserve + .checked_sub(token_bought) + .map_err(StdError::overflow)?; + Ok(output_token) + }, + )?; + + Ok(Response::new().add_messages(msgs).add_attributes(vec![ + attr("native_sold", input_amount), + attr("token_bought", token_bought), + ])) +} +``` +:: +Users can swap tokens using the AMM by calling the execute_swap function. They specify the input token (input_token), the amount to swap (input_amount), and the minimum output amount (min_output). The function calculates the output amount based on the constant product formula and checks if it meets the minimum requirement. If the swap is valid, it transfers the input token from the user to the contract and transfers the output token back to the user. + +### Configuration Update +To update the AMM configuration +::highlight-card +```rust +pub fn execute_update_config( + deps: DepsMut, + info: MessageInfo, + new_owner: Option, + lp_fee_percent: Decimal, + protocol_fee_percent: Decimal, + protocol_fee_recipient: String, +) -> Result { + let owner = OWNER.load(deps.storage)?; + if Some(info.sender) != owner { + return Err(ContractError::Unauthorized {}); + } + + let new_owner_addr = new_owner + .as_ref() + .map(|h| deps.api.addr_validate(h)) + .transpose()?; + OWNER.save(deps.storage, &new_owner_addr)?; + + let total_fee_percent = lp_fee_percent + protocol_fee_percent; + let max_fee_percent = Decimal::from_str(MAX_FEE_PERCENT)?; + if total_fee_percent > max_fee_percent { + return Err(ContractError::FeesTooHigh { + max_fee_percent, + total_fee_percent, + }); + } + + let protocol_fee_recipient = deps.api.addr_validate(&protocol_fee_recipient)?; + let updated_fees = Fees { + protocol_fee_recipient: protocol_fee_recipient.clone(), + lp_fee_percent, + protocol_fee_percent, + }; + FEES.save(deps.storage, &updated_fees)?; + + let new_owner = new_owner.unwrap_or_default(); + Ok(Response::new().add_attributes(vec![ + attr("new_owner", new_owner), + attr("lp_fee_percent", lp_fee_percent.to_string()), + attr("protocol_fee_percent", protocol_fee_percent.to_string()), + attr("protocol_fee_recipient", protocol_fee_recipient.to_string()), + ])) +} +``` +:: +The AMM's configuration can be updated by the owner using the execute_update_config function. The owner can change the LP (liquidity provider) fee percentage, the protocol fee percentage, and the protocol fee recipient address. + +### Deposit Freezing +To freeze the deposit to AMM +::highlight-card +```rust +fn execute_freeze_deposits( + deps: DepsMut, + sender: Addr, + freeze: bool, +) -> Result { + if let Some(owner) = OWNER.load(deps.storage)? { + if sender != owner { + return Err(ContractError::UnauthorizedPoolFreeze {}); + } + } else { + return Err(ContractError::UnauthorizedPoolFreeze {}); + } + + FROZEN.save(deps.storage, &freeze)?; + Ok(Response::new().add_attribute("action", "freezing-contracts")) +} + +fn check_expiration( + expiration: &Option, + block: &BlockInfo, +) -> Result<(), ContractError> { + match expiration { + Some(e) => { + if e.is_expired(block) { + return Err(ContractError::MsgExpirationError {}); + } + Ok(()) + } + None => Ok(()), + } +} +``` +:: +The owner can freeze deposits to the AMM by calling the execute_freeze_deposits function. This prevents users from adding liquidity or swapping tokens. Only the owner can freeze or unfreeze deposits. + + +## Example + +## Example +To create an AMM with CosmWasm, you can create the following files: +lib.rs +integration_tests.rs +contract.rs +msg.rs +error.rs +state.rs + +### lib.rs + +::highlight-card +```rust +pub mod contract; +pub mod error; +mod integration_test; +pub mod msg; +pub mod state; +``` +:: + +### integration_tests.rs +::highlight-card +```rust +#![cfg(test)] + +use std::borrow::BorrowMut; + +use crate::error::ContractError; +use cosmwasm_std::{coins, Addr, Coin, Decimal, Empty, Uint128}; +use cw20::{Cw20Coin, Cw20Contract, Cw20ExecuteMsg, Denom}; +use cw_multi_test::{App, Contract, ContractWrapper, Executor}; +use std::str::FromStr; + +use crate::msg::{ExecuteMsg, FeeResponse, InfoResponse, InstantiateMsg, QueryMsg, TokenSelect}; + +fn mock_app() -> App { + App::default() +} + +pub fn contract_amm() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ); + Box::new(contract) +} + +pub fn contract_cw20() -> Box> { + let contract = ContractWrapper::new( + cw20_base::contract::execute, + cw20_base::contract::instantiate, + cw20_base::contract::query, + ); + Box::new(contract) +} + +fn get_info(router: &App, contract_addr: &Addr) -> InfoResponse { + router + .wrap() + .query_wasm_smart(contract_addr, &QueryMsg::Info {}) + .unwrap() +} + +fn get_fee(router: &App, contract_addr: &Addr) -> FeeResponse { + router + .wrap() + .query_wasm_smart(contract_addr, &QueryMsg::Fee {}) + .unwrap() +} + +fn create_amm( + router: &mut App, + owner: &Addr, + token1_denom: Denom, + token2_denom: Denom, + lp_fee_percent: Decimal, + protocol_fee_percent: Decimal, + protocol_fee_recipient: String, +) -> Addr { + // set up amm contract + let amm_id = router.store_code(contract_amm()); + let msg = InstantiateMsg { + token1_denom, + token2_denom, + owner: Some(owner.to_string()), + lp_fee_percent, + protocol_fee_percent, + protocol_fee_recipient, + }; + router + .instantiate_contract(amm_id, owner.clone(), &msg, &[], "amm", None) + .unwrap() +} + +// CreateCW20 create new cw20 with given initial balance belonging to owner +fn create_cw20( + router: &mut App, + owner: &Addr, + name: String, + symbol: String, + balance: Uint128, +) -> Cw20Contract { + // set up cw20 contract with some tokens + let cw20_id = router.store_code(contract_cw20()); + let msg = cw20_base::msg::InstantiateMsg { + name, + symbol, + decimals: 6, + initial_balances: vec![Cw20Coin { + address: owner.to_string(), + amount: balance, + }], + mint: None, + marketing: None, + }; + let addr = router + .instantiate_contract(cw20_id, owner.clone(), &msg, &[], "CASH", None) + .unwrap(); + Cw20Contract(addr) +} + +fn bank_balance(router: &mut App, addr: &Addr, denom: String) -> Coin { + router + .wrap() + .query_balance(addr.to_string(), denom) + .unwrap() +} + +#[test] +// receive cw20 tokens and release upon approval +fn test_instantiate() { + let mut router = mock_app(); + + const NATIVE_TOKEN_DENOM: &str = "juno"; + + let owner = Addr::unchecked("owner"); + let funds = coins(2000, NATIVE_TOKEN_DENOM); + router.borrow_mut().init_modules(|router, _, storage| { + router.bank.init_balance(storage, &owner, funds).unwrap() + }); + + let cw20_token = create_cw20( + &mut router, + &owner, + "token".to_string(), + "CWTOKEN".to_string(), + Uint128::new(5000), + ); + + let lp_fee_percent = Decimal::from_str("0.3").unwrap(); + let protocol_fee_percent = Decimal::zero(); + let amm_addr = create_amm( + &mut router, + &owner, + Denom::Native(NATIVE_TOKEN_DENOM.into()), + Denom::Cw20(cw20_token.addr()), + lp_fee_percent, + protocol_fee_percent, + owner.to_string(), + ); + + assert_ne!(cw20_token.addr(), amm_addr); + + let _info = get_info(&router, &amm_addr); + + let fee = get_fee(&router, &amm_addr); + assert_eq!(fee.lp_fee_percent, lp_fee_percent); + assert_eq!(fee.protocol_fee_percent, protocol_fee_percent); + assert_eq!(fee.protocol_fee_recipient, owner.to_string()); + assert_eq!(fee.owner.unwrap(), owner.to_string()); + + // Test instantiation with invalid fee amount + let lp_fee_percent = Decimal::from_str("1.01").unwrap(); + let protocol_fee_percent = Decimal::zero(); + let amm_id = router.store_code(contract_amm()); + let msg = InstantiateMsg { + token1_denom: Denom::Native(NATIVE_TOKEN_DENOM.into()), + token2_denom: Denom::Cw20(cw20_token.addr()), + owner: Some(owner.to_string()), + lp_fee_percent, + protocol_fee_percent, + protocol_fee_recipient: owner.to_string(), + }; + let err = router + .instantiate_contract(amm_id, owner.clone(), &msg, &[], "amm", None) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + ContractError::FeesTooHigh { + max_fee_percent: Decimal::from_str("1").unwrap(), + total_fee_percent: Decimal::from_str("1.01").unwrap() + }, + err + ); +} + + +#[test] +fn update_config() { + let mut router = mock_app(); + + const NATIVE_TOKEN_DENOM: &str = "juno"; + + let owner = Addr::unchecked("owner"); + let funds = coins(2000, NATIVE_TOKEN_DENOM); + router.borrow_mut().init_modules(|router, _, storage| { + router.bank.init_balance(storage, &owner, funds).unwrap() + }); + + let cw20_token = create_cw20( + &mut router, + &owner, + "token".to_string(), + "CWTOKEN".to_string(), + Uint128::new(5000), + ); + + let lp_fee_percent = Decimal::from_str("0.3").unwrap(); + let protocol_fee_percent = Decimal::zero(); + let amm_addr = create_amm( + &mut router, + &owner, + Denom::Native(NATIVE_TOKEN_DENOM.to_string()), + Denom::Cw20(cw20_token.addr()), + lp_fee_percent, + protocol_fee_percent, + owner.to_string(), + ); + + let lp_fee_percent = Decimal::from_str("0.15").unwrap(); + let protocol_fee_percent = Decimal::from_str("0.15").unwrap(); + let msg = ExecuteMsg::UpdateConfig { + owner: Some(owner.to_string()), + protocol_fee_recipient: "new_fee_recpient".to_string(), + lp_fee_percent, + protocol_fee_percent, + }; + let _res = router + .execute_contract(owner.clone(), amm_addr.clone(), &msg, &[]) + .unwrap(); + + let fee = get_fee(&router, &amm_addr); + assert_eq!(fee.protocol_fee_recipient, "new_fee_recpient".to_string()); + assert_eq!(fee.protocol_fee_percent, protocol_fee_percent); + assert_eq!(fee.lp_fee_percent, lp_fee_percent); + assert_eq!(fee.owner.unwrap(), owner.to_string()); + + // Try updating config with fee values that are too high + let lp_fee_percent = Decimal::from_str("1.01").unwrap(); + let protocol_fee_percent = Decimal::zero(); + let msg = ExecuteMsg::UpdateConfig { + owner: Some(owner.to_string()), + protocol_fee_recipient: "new_fee_recpient".to_string(), + lp_fee_percent, + protocol_fee_percent, + }; + let err = router + .execute_contract(owner.clone(), amm_addr.clone(), &msg, &[]) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + ContractError::FeesTooHigh { + max_fee_percent: Decimal::from_str("1").unwrap(), + total_fee_percent: Decimal::from_str("1.01").unwrap() + }, + err + ); + + // Try updating config with invalid owner, show throw unauthoritzed error + let lp_fee_percent = Decimal::from_str("0.21").unwrap(); + let protocol_fee_percent = Decimal::from_str("0.09").unwrap(); + let msg = ExecuteMsg::UpdateConfig { + owner: Some(owner.to_string()), + protocol_fee_recipient: owner.to_string(), + lp_fee_percent, + protocol_fee_percent, + }; + let err = router + .execute_contract( + Addr::unchecked("invalid_owner"), + amm_addr.clone(), + &msg, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(ContractError::Unauthorized {}, err); + + // Try updating owner and fee params + let msg = ExecuteMsg::UpdateConfig { + owner: Some("new_owner".to_string()), + protocol_fee_recipient: owner.to_string(), + lp_fee_percent, + protocol_fee_percent, + }; + let _res = router + .execute_contract(owner.clone(), amm_addr.clone(), &msg, &[]) + .unwrap(); + + let fee = get_fee(&router, &amm_addr); + assert_eq!(fee.protocol_fee_recipient, owner.to_string()); + assert_eq!(fee.protocol_fee_percent, protocol_fee_percent); + assert_eq!(fee.lp_fee_percent, lp_fee_percent); + assert_eq!(fee.owner.unwrap(), "new_owner".to_string()); +} + +#[test] +fn test_pass_through_swap() { + let mut router = mock_app(); + + const NATIVE_TOKEN_DENOM: &str = "juno"; + + let owner = Addr::unchecked("owner"); + let funds = coins(2000, NATIVE_TOKEN_DENOM); + router.borrow_mut().init_modules(|router, _, storage| { + router.bank.init_balance(storage, &owner, funds).unwrap() + }); + + let token1 = create_cw20( + &mut router, + &owner, + "token1".to_string(), + "TOKENONE".to_string(), + Uint128::new(5000), + ); + let token2 = create_cw20( + &mut router, + &owner, + "token2".to_string(), + "TOKENTWO".to_string(), + Uint128::new(5000), + ); + + let lp_fee_percent = Decimal::from_str("0.03").unwrap(); + let protocol_fee_percent = Decimal::zero(); + let amm = create_amm( + &mut router, + &owner, + Denom::Cw20(token1.addr()), + Denom::Cw20(token2.addr()), + lp_fee_percent, + protocol_fee_percent, + owner.to_string(), + ); + + // Add initial liquidity to both pools + let allowance_msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: amm.to_string(), + amount: Uint128::new(100), + expires: None, + }; + let _res = router + .execute_contract(owner.clone(), token1.addr(), &allowance_msg, &[]) + .unwrap(); + let _res = router + .execute_contract(owner.clone(), token2.addr(), &allowance_msg, &[]) + .unwrap(); + + let add_liquidity_msg = ExecuteMsg::AddLiquidity { + token1_amount: Uint128::new(100), + min_liquidity: Uint128::new(100), + token2_amount: Uint128::new(100), + expiration: None, + }; + router + .execute_contract( + owner.clone(), + amm.clone(), + &add_liquidity_msg, + &[Coin { + denom: NATIVE_TOKEN_DENOM.into(), + amount: Uint128::zero(), + }], + ) + .unwrap(); + + // Swap token1 for token2 + let allowance_msg = Cw20ExecuteMsg::IncreaseAllowance { + spender: amm.to_string(), + amount: Uint128::new(10), + expires: None, + }; + let _res = router + .execute_contract(owner.clone(), token1.addr(), &allowance_msg, &[]) + .unwrap(); + + let swap_msg = ExecuteMsg::Swap { + input_token: TokenSelect::Token1, + input_amount: Uint128::new(10), + min_output: Uint128::new(8), + expiration: None, + }; + let _res = router + .execute_contract(owner.clone(), amm.clone(), &swap_msg, &[]) + .unwrap(); + + // ensure balances updated + let token1_balance = token1.balance(&router, owner.clone()).unwrap(); + assert_eq!(token1_balance, Uint128::new(4890)); + + let token2_balance = token2.balance(&router, owner.clone()).unwrap(); + assert_eq!(token2_balance, Uint128::new(4909)); + + let amm_native_balance = bank_balance(&mut router, &amm, NATIVE_TOKEN_DENOM.to_string()); + assert_eq!(amm_native_balance.amount, Uint128::zero()); + + // assert internal state is consistent + let info_amm: InfoResponse = get_info(&router, &amm); + println!("{:?}", info_amm); + let token1_balance = token1.balance(&router, amm.clone()).unwrap(); + let token2_balance = token2.balance(&router, amm.clone()).unwrap(); + println!("{} {}", token1_balance, token2_balance); + assert_eq!(info_amm.token2_reserve, token2_balance); + assert_eq!(info_amm.token1_reserve, token1_balance); +} +``` +:: + +### contract.rs + +::highlight-card +```rust +use cosmwasm_std::{ + attr, entry_point, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, Deps, DepsMut, + Env, MessageInfo, Response, StdError, StdResult, Uint128, Uint256, Uint512, + WasmMsg, +}; +use cw2::set_contract_version; +use cw20::Denom::Cw20; +use cw20::{Cw20ExecuteMsg, Denom, Expiration}; +use cw20_base::contract::query_balance; +use std::convert::TryInto; +use std::str::FromStr; + +use crate::error::ContractError; +use crate::msg::{ + ExecuteMsg, FeeResponse, InfoResponse, InstantiateMsg, QueryMsg, Token1ForToken2PriceResponse, + Token2ForToken1PriceResponse, TokenSelect, +}; +use crate::state::{Fees, Token, FEES, FROZEN, OWNER, TOKEN1, TOKEN2}; + +// Version info for migration info +pub const CONTRACT_NAME: &str = "crates.io:product-amm"; +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const FEE_SCALE_FACTOR: Uint128 = Uint128::new(10_000); +const MAX_FEE_PERCENT: &str = "1"; +const FEE_DECIMAL_PRECISION: Uint128 = Uint128::new(10u128.pow(20)); + +// Note, you can use StdResult in some functions where you do not +// make use of the custom errors +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let token1 = Token { + reserve: Uint128::zero(), + denom: msg.token1_denom.clone(), + }; + TOKEN1.save(deps.storage, &token1)?; + + let token2 = Token { + denom: msg.token2_denom.clone(), + reserve: Uint128::zero(), + }; + TOKEN2.save(deps.storage, &token2)?; + + let owner = msg.owner.map(|h| deps.api.addr_validate(&h)).transpose()?; + OWNER.save(deps.storage, &owner)?; + + let protocol_fee_recipient = deps.api.addr_validate(&msg.protocol_fee_recipient)?; + let total_fee_percent = msg.lp_fee_percent + msg.protocol_fee_percent; + let max_fee_percent = Decimal::from_str(MAX_FEE_PERCENT)?; + if total_fee_percent > max_fee_percent { + return Err(ContractError::FeesTooHigh { + max_fee_percent, + total_fee_percent, + }); + } + + let fees = Fees { + lp_fee_percent: msg.lp_fee_percent, + protocol_fee_percent: msg.protocol_fee_percent, + protocol_fee_recipient, + }; + FEES.save(deps.storage, &fees)?; + + // Depositing is not frozen by default + FROZEN.save(deps.storage, &false)?; + + Ok(Response::new().add_attribute("key", "instantiate")) +} + +// And declare a custom Error variant for the ones where you will want to make use of it +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::AddLiquidity { + token1_amount, + min_liquidity, + expiration, token2_amount } => { + if FROZEN.load(deps.storage)? { + return Err(ContractError::FrozenPool {}); + } + execute_add_liquidity( + deps, + &info, + env, + min_liquidity, + token1_amount, + token2_amount, + expiration, + ) + } + ExecuteMsg::RemoveLiquidity { + amount, + min_token1, + min_token2, + expiration, + } => execute_remove_liquidity(deps, info, env, amount, min_token1, min_token2, expiration), + ExecuteMsg::Swap { + input_token, + input_amount, + min_output, + expiration, + .. + } => { + if FROZEN.load(deps.storage)? { + return Err(ContractError::FrozenPool {}); + } + execute_swap( + deps, + &info, + input_amount, + env, + input_token, + info.sender.to_string(), + min_output, + expiration, + ) + } + ExecuteMsg::UpdateConfig { + owner, + protocol_fee_recipient, + lp_fee_percent, + protocol_fee_percent, + } => execute_update_config( + deps, + info, + owner, + lp_fee_percent, + protocol_fee_percent, + protocol_fee_recipient, + ), + ExecuteMsg::FreezeDeposits { freeze } => execute_freeze_deposits(deps, info.sender, freeze), + } +} + +fn execute_freeze_deposits( + deps: DepsMut, + sender: Addr, + freeze: bool, +) -> Result { + if let Some(owner) = OWNER.load(deps.storage)? { + if sender != owner { + return Err(ContractError::UnauthorizedPoolFreeze {}); + } + } else { + return Err(ContractError::UnauthorizedPoolFreeze {}); + } + + FROZEN.save(deps.storage, &freeze)?; + Ok(Response::new().add_attribute("action", "freezing-contracts")) +} + +fn check_expiration( + expiration: &Option, + block: &BlockInfo, +) -> Result<(), ContractError> { + match expiration { + Some(e) => { + if e.is_expired(block) { + return Err(ContractError::MsgExpirationError {}); + } + Ok(()) + } + None => Ok(()), + } +} + + +pub fn execute_add_liquidity( + deps: DepsMut, + info: &MessageInfo, + env: Env, + min_liquidity: Uint128, + token1_amount: Uint128, + token2_amount: Uint128, + expiration: Option, +) -> Result { + check_expiration(&expiration, &env.block)?; + + let token1 = TOKEN1.load(deps.storage)?; + let token2 = TOKEN2.load(deps.storage)?; + + let liquidity_amount = token1_amount+token2_amount; + + if liquidity_amount < min_liquidity { + return Err(ContractError::MinLiquidityError { + min_liquidity, + liquidity_available: liquidity_amount, + }); + } + + // Generate cw20 transfer messages if necessary + let mut transfer_msgs: Vec = vec![]; + if let Cw20(addr) = token1.denom { + transfer_msgs.push(get_cw20_transfer_from_msg( + &info.sender, + &env.contract.address, + &addr, + token1_amount, + )?) + } + if let Cw20(addr) = token2.denom.clone() { + transfer_msgs.push(get_cw20_transfer_from_msg( + &info.sender, + &env.contract.address, + &addr, + token2_amount, + )?) + } + + + TOKEN1.update(deps.storage, |mut token1| -> Result<_, ContractError> { + token1.reserve += token1_amount; + Ok(token1) + })?; + TOKEN2.update(deps.storage, |mut token2| -> Result<_, ContractError> { + token2.reserve += token2_amount; + Ok(token2) + })?; + + Ok(Response::new() + .add_messages(transfer_msgs) + .add_attributes(vec![ + attr("token1_amount", token1_amount), + attr("token2_amount", token2_amount), + attr("liquidity_received", liquidity_amount), + ])) +} + +fn get_cw20_transfer_from_msg( + owner: &Addr, + recipient: &Addr, + token_addr: &Addr, + token_amount: Uint128, +) -> StdResult { + // create transfer cw20 msg + let transfer_cw20_msg = Cw20ExecuteMsg::TransferFrom { + owner: owner.into(), + recipient: recipient.into(), + amount: token_amount, + }; + let exec_cw20_transfer = WasmMsg::Execute { + contract_addr: token_addr.into(), + msg: to_binary(&transfer_cw20_msg)?, + funds: vec![], + }; + let cw20_transfer_cosmos_msg: CosmosMsg = exec_cw20_transfer.into(); + Ok(cw20_transfer_cosmos_msg) +} + +pub fn execute_update_config( + deps: DepsMut, + info: MessageInfo, + new_owner: Option, + lp_fee_percent: Decimal, + protocol_fee_percent: Decimal, + protocol_fee_recipient: String, +) -> Result { + let owner = OWNER.load(deps.storage)?; + if Some(info.sender) != owner { + return Err(ContractError::Unauthorized {}); + } + + let new_owner_addr = new_owner + .as_ref() + .map(|h| deps.api.addr_validate(h)) + .transpose()?; + OWNER.save(deps.storage, &new_owner_addr)?; + + let total_fee_percent = lp_fee_percent + protocol_fee_percent; + let max_fee_percent = Decimal::from_str(MAX_FEE_PERCENT)?; + if total_fee_percent > max_fee_percent { + return Err(ContractError::FeesTooHigh { + max_fee_percent, + total_fee_percent, + }); + } + + let protocol_fee_recipient = deps.api.addr_validate(&protocol_fee_recipient)?; + let updated_fees = Fees { + protocol_fee_recipient: protocol_fee_recipient.clone(), + lp_fee_percent, + protocol_fee_percent, + }; + FEES.save(deps.storage, &updated_fees)?; + + let new_owner = new_owner.unwrap_or_default(); + Ok(Response::new().add_attributes(vec![ + attr("new_owner", new_owner), + attr("lp_fee_percent", lp_fee_percent.to_string()), + attr("protocol_fee_percent", protocol_fee_percent.to_string()), + attr("protocol_fee_recipient", protocol_fee_recipient.to_string()), + ])) +} + +pub fn execute_remove_liquidity( + deps: DepsMut, + info: MessageInfo, + env: Env, + amount: Uint128, + min_token1: Uint128, + min_token2: Uint128, + expiration: Option, +) -> Result { + check_expiration(&expiration, &env.block)?; + + + let token1 = TOKEN1.load(deps.storage)?; + let token2 = TOKEN2.load(deps.storage)?; + let total_token_supply = token1.reserve+token2.reserve; + + if amount > total_token_supply { + return Err(ContractError::InsufficientLiquidityError { + requested: amount, + available: total_token_supply, + }); + } + + let token1_amount = amount + .checked_mul(token1.reserve) + .map_err(StdError::overflow)? + .checked_div(total_token_supply) + .map_err(StdError::divide_by_zero)?; + if token1_amount < min_token1 { + return Err(ContractError::MinToken1Error { + requested: min_token1, + available: token1_amount, + }); + } + + let token2_amount = amount + .checked_mul(token2.reserve) + .map_err(StdError::overflow)? + .checked_div(total_token_supply) + .map_err(StdError::divide_by_zero)?; + if token2_amount < min_token2 { + return Err(ContractError::MinToken2Error { + requested: min_token2, + available: token2_amount, + }); + } + + TOKEN1.update(deps.storage, |mut token1| -> Result<_, ContractError> { + token1.reserve = token1 + .reserve + .checked_sub(token1_amount) + .map_err(StdError::overflow)?; + Ok(token1) + })?; + + TOKEN2.update(deps.storage, |mut token2| -> Result<_, ContractError> { + token2.reserve = token2 + .reserve + .checked_sub(token2_amount) + .map_err(StdError::overflow)?; + Ok(token2) + })?; + + let token1_transfer_msg = match token1.denom { + Denom::Cw20(addr) => get_cw20_transfer_to_msg(&info.sender, &addr, token1_amount)?, + Denom::Native(_denom) => {unimplemented!()}, + }; + let token2_transfer_msg = match token2.denom { + Denom::Cw20(addr) => get_cw20_transfer_to_msg(&info.sender, &addr, token2_amount)?, + Denom::Native(_denom) => {unimplemented!()}, + }; + + Ok(Response::new() + .add_messages(vec![ + token1_transfer_msg, + token2_transfer_msg, + ]) + .add_attributes(vec![ + attr("liquidity_burned", amount), + attr("token1_returned", token1_amount), + attr("token2_returned", token2_amount), + ])) +} + +fn get_cw20_transfer_to_msg( + recipient: &Addr, + token_addr: &Addr, + token_amount: Uint128, +) -> StdResult { + // create transfer cw20 msg + let transfer_cw20_msg = Cw20ExecuteMsg::Transfer { + recipient: recipient.into(), + amount: token_amount, + }; + let exec_cw20_transfer = WasmMsg::Execute { + contract_addr: token_addr.into(), + msg: to_binary(&transfer_cw20_msg)?, + funds: vec![], + }; + let cw20_transfer_cosmos_msg: CosmosMsg = exec_cw20_transfer.into(); + Ok(cw20_transfer_cosmos_msg) +} + + +fn get_fee_transfer_msg( + sender: &Addr, + recipient: &Addr, + fee_denom: &Denom, + amount: Uint128, +) -> StdResult { + match fee_denom { + Denom::Cw20(addr) => get_cw20_transfer_from_msg(sender, recipient, addr, amount), + Denom::Native(_denom) => {unimplemented!()}, + } +} + +fn fee_decimal_to_uint128(decimal: Decimal) -> StdResult { + let result: Uint128 = decimal + .atomics() + .checked_mul(FEE_SCALE_FACTOR) + .map_err(StdError::overflow)?; + + Ok(result / FEE_DECIMAL_PRECISION) +} + +fn get_input_price( + input_amount: Uint128, + input_reserve: Uint128, + output_reserve: Uint128, + fee_percent: Decimal, +) -> StdResult { + if input_reserve == Uint128::zero() || output_reserve == Uint128::zero() { + return Err(StdError::generic_err("No liquidity")); + }; + + let fee_percent = fee_decimal_to_uint128(fee_percent)?; + let fee_reduction_percent = FEE_SCALE_FACTOR - fee_percent; + let input_amount_with_fee = Uint512::from(input_amount.full_mul(fee_reduction_percent)); + let numerator = input_amount_with_fee + .checked_mul(Uint512::from(output_reserve)) + .map_err(StdError::overflow)?; + let denominator = Uint512::from(input_reserve) + .checked_mul(Uint512::from(FEE_SCALE_FACTOR)) + .map_err(StdError::overflow)? + .checked_add(input_amount_with_fee) + .map_err(StdError::overflow)?; + + Ok(numerator + .checked_div(denominator) + .map_err(StdError::divide_by_zero)? + .try_into()?) +} + +fn get_protocol_fee_amount(input_amount: Uint128, fee_percent: Decimal) -> StdResult { + if fee_percent.is_zero() { + return Ok(Uint128::zero()); + } + + let fee_percent = fee_decimal_to_uint128(fee_percent)?; + Ok(input_amount + .full_mul(fee_percent) + .checked_div(Uint256::from(FEE_SCALE_FACTOR)) + .map_err(StdError::divide_by_zero)? + .try_into()?) +} + +#[allow(clippy::too_many_arguments)] +pub fn execute_swap( + deps: DepsMut, + info: &MessageInfo, + input_amount: Uint128, + _env: Env, + input_token_enum: TokenSelect, + recipient: String, + min_token: Uint128, + expiration: Option, +) -> Result { + check_expiration(&expiration, &_env.block)?; + + let input_token_item = match input_token_enum { + TokenSelect::Token1 => TOKEN1, + TokenSelect::Token2 => TOKEN2, + }; + let input_token = input_token_item.load(deps.storage)?; + let output_token_item = match input_token_enum { + TokenSelect::Token1 => TOKEN2, + TokenSelect::Token2 => TOKEN1, + }; + let output_token = output_token_item.load(deps.storage)?; + + + let fees = FEES.load(deps.storage)?; + let total_fee_percent = fees.lp_fee_percent + fees.protocol_fee_percent; + let token_bought = get_input_price( + input_amount, + input_token.reserve, + output_token.reserve, + total_fee_percent, + )?; + + if min_token > token_bought { + return Err(ContractError::SwapMinError { + min: min_token, + available: token_bought, + }); + } + // Calculate fees + let protocol_fee_amount = get_protocol_fee_amount(input_amount, fees.protocol_fee_percent)?; + let input_amount_minus_protocol_fee = input_amount - protocol_fee_amount; + + let mut msgs = match input_token.denom.clone() { + Denom::Cw20(addr) => vec![get_cw20_transfer_from_msg( + &info.sender, + &_env.contract.address, + &addr, + input_amount_minus_protocol_fee, + )?], + Denom::Native(_) => vec![], + }; + + // Send protocol fee to protocol fee recipient + if !protocol_fee_amount.is_zero() { + msgs.push(get_fee_transfer_msg( + &info.sender, + &fees.protocol_fee_recipient, + &input_token.denom, + protocol_fee_amount, + )?) + } + + let recipient = deps.api.addr_validate(&recipient)?; + // Create transfer to message + msgs.push(match output_token.denom { + Denom::Cw20(addr) => get_cw20_transfer_to_msg(&recipient, &addr, token_bought)?, + Denom::Native(_denom) => {unimplemented!()}, + }); + + input_token_item.update( + deps.storage, + |mut input_token| -> Result<_, ContractError> { + input_token.reserve = input_token + .reserve + .checked_add(input_amount_minus_protocol_fee) + .map_err(StdError::overflow)?; + Ok(input_token) + }, + )?; + + output_token_item.update( + deps.storage, + |mut output_token| -> Result<_, ContractError> { + output_token.reserve = output_token + .reserve + .checked_sub(token_bought) + .map_err(StdError::overflow)?; + Ok(output_token) + }, + )?; + + Ok(Response::new().add_messages(msgs).add_attributes(vec![ + attr("native_sold", input_amount), + attr("token_bought", token_bought), + ])) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?), + QueryMsg::Info {} => to_binary(&query_info(deps)?), + QueryMsg::Token1ForToken2Price { token1_amount } => { + to_binary(&query_token1_for_token2_price(deps, token1_amount)?) + } + QueryMsg::Token2ForToken1Price { token2_amount } => { + to_binary(&query_token2_for_token1_price(deps, token2_amount)?) + } + QueryMsg::Fee {} => to_binary(&query_fee(deps)?), + } +} + +pub fn query_info(deps: Deps) -> StdResult { + let token1 = TOKEN1.load(deps.storage)?; + let token2 = TOKEN2.load(deps.storage)?; + + // TODO get total supply + Ok(InfoResponse { + token1_reserve: token1.reserve, + token1_denom: token1.denom, + token2_reserve: token2.reserve, + token2_denom: token2.denom, + + }) +} + +pub fn query_token1_for_token2_price( + deps: Deps, + token1_amount: Uint128, +) -> StdResult { + let token1 = TOKEN1.load(deps.storage)?; + let token2 = TOKEN2.load(deps.storage)?; + + let fees = FEES.load(deps.storage)?; + let total_fee_percent = fees.lp_fee_percent + fees.protocol_fee_percent; + let token2_amount = get_input_price( + token1_amount, + token1.reserve, + token2.reserve, + total_fee_percent, + )?; + Ok(Token1ForToken2PriceResponse { token2_amount }) +} + +pub fn query_token2_for_token1_price( + deps: Deps, + token2_amount: Uint128, +) -> StdResult { + let token1 = TOKEN1.load(deps.storage)?; + let token2 = TOKEN2.load(deps.storage)?; + + let fees = FEES.load(deps.storage)?; + let total_fee_percent = fees.lp_fee_percent + fees.protocol_fee_percent; + let token1_amount = get_input_price( + token2_amount, + token2.reserve, + token1.reserve, + total_fee_percent, + )?; + Ok(Token2ForToken1PriceResponse { token1_amount }) +} + +pub fn query_fee(deps: Deps) -> StdResult { + let fees = FEES.load(deps.storage)?; + let owner = OWNER.load(deps.storage)?.map(|o| o.into_string()); + + Ok(FeeResponse { + owner, + lp_fee_percent: fees.lp_fee_percent, + protocol_fee_percent: fees.protocol_fee_percent, + protocol_fee_recipient: fees.protocol_fee_recipient.into_string(), + }) +} + + + +#[cfg(test)] +mod tests { + use super::*; + + + #[test] + fn test_get_input_price() { + let fee_percent = Decimal::from_str("0.03").unwrap(); + // Base case + assert_eq!( + get_input_price( + Uint128::new(10), + Uint128::new(100), + Uint128::new(100), + fee_percent + ) + .unwrap(), + Uint128::new(9) + ); + + // No input reserve error + let err = get_input_price( + Uint128::new(10), + Uint128::new(0), + Uint128::new(100), + fee_percent, + ) + .unwrap_err(); + assert_eq!(err, StdError::generic_err("No liquidity")); + + // No output reserve error + let err = get_input_price( + Uint128::new(10), + Uint128::new(100), + Uint128::new(0), + fee_percent, + ) + .unwrap_err(); + assert_eq!(err, StdError::generic_err("No liquidity")); + + // No reserve error + let err = get_input_price( + Uint128::new(10), + Uint128::new(0), + Uint128::new(0), + fee_percent, + ) + .unwrap_err(); + assert_eq!(err, StdError::generic_err("No liquidity")); + } +} + +``` +:: + + +### msg.rs +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{Decimal, Uint128}; + +use cw20::{Denom, Expiration}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InstantiateMsg { + pub token1_denom: Denom, + pub token2_denom: Denom, + pub owner: Option, + pub protocol_fee_recipient: String, + // NOTE: Fees percents are out of 100 e.g., 1 = 1% + pub protocol_fee_percent: Decimal, + pub lp_fee_percent: Decimal, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub enum TokenSelect { + Token1, + Token2, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + AddLiquidity { + token1_amount: Uint128, + token2_amount: Uint128, + min_liquidity: Uint128, + expiration: Option, + }, + RemoveLiquidity { + amount: Uint128, + min_token1: Uint128, + min_token2: Uint128, + expiration: Option, + }, + Swap { + input_token: TokenSelect, + input_amount: Uint128, + min_output: Uint128, + expiration: Option, + }, + UpdateConfig { + owner: Option, + lp_fee_percent: Decimal, + protocol_fee_percent: Decimal, + protocol_fee_recipient: String, + }, + // Freeze adding new deposits + FreezeDeposits { + freeze: bool, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// Implements CW20. Returns the current balance of the given address, 0 if unset. + Balance { + address: String, + }, + Info {}, + Token1ForToken2Price { + token1_amount: Uint128, + }, + Token2ForToken1Price { + token2_amount: Uint128, + }, + Fee {}, +} +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct MigrateMsg { + pub owner: Option, + pub protocol_fee_recipient: String, + pub protocol_fee_percent: Decimal, + pub lp_fee_percent: Decimal, + pub freeze_pool: bool, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct InfoResponse { + pub token1_reserve: Uint128, + pub token1_denom: Denom, + pub token2_reserve: Uint128, + pub token2_denom: Denom, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +pub struct FeeResponse { + pub owner: Option, + pub lp_fee_percent: Decimal, + pub protocol_fee_percent: Decimal, + pub protocol_fee_recipient: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct Token1ForToken2PriceResponse { + pub token2_amount: Uint128, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct Token2ForToken1PriceResponse { + pub token1_amount: Uint128, +} + +``` +:: + +### error.rs + +::highlight-card +```rust +use cosmwasm_std::{Decimal, StdError, Uint128}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + Cw20Error(#[from] cw20_base::ContractError), + + #[error("None Error")] + NoneError {}, + + #[error("Unauthorized")] + Unauthorized {}, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. + #[error("Min liquidity error: requested: {min_liquidity}, available: {liquidity_available}")] + MinLiquidityError { + min_liquidity: Uint128, + liquidity_available: Uint128, + }, + + #[error("Max token error: max_token: {max_token}, tokens_required: {tokens_required}")] + MaxTokenError { + max_token: Uint128, + tokens_required: Uint128, + }, + + #[error("Insufficient liquidity error: requested: {requested}, available: {available}")] + InsufficientLiquidityError { + requested: Uint128, + available: Uint128, + }, + + #[error("Min token1 error: requested: {requested}, available: {available}")] + MinToken1Error { + requested: Uint128, + available: Uint128, + }, + + #[error("Min token2 error: requested: {requested}, available: {available}")] + MinToken2Error { + requested: Uint128, + available: Uint128, + }, + + #[error("Incorrect native denom: provided: {provided}, required: {required}")] + IncorrectNativeDenom { provided: String, required: String }, + + #[error("Swap min error: min: {min}, available: {available}")] + SwapMinError { min: Uint128, available: Uint128 }, + + #[error("MsgExpirationError")] + MsgExpirationError {}, + + #[error("Total fee ({total_fee_percent}) percent is higher than max ({max_fee_percent})")] + FeesTooHigh { + max_fee_percent: Decimal, + total_fee_percent: Decimal, + }, + + #[error("InsufficientFunds")] + InsufficientFunds {}, + + #[error("Uknown reply id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Failed to instantiate lp token")] + InstantiateLpTokenError {}, + + #[error("The output amm provided is invalid")] + InvalidOutputPool {}, + + #[error("Unauthorized pool freeze - sender is not an owner or owner has not been set")] + UnauthorizedPoolFreeze {}, + + #[error("This pools is frozen - you can not deposit or swap tokens")] + FrozenPool {}, +} + +``` +:: + +### state.rs +::highlight-card +```rust +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{Addr, Decimal, Uint128}; +use cw20::Denom; +use cw_storage_plus::Item; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Token { + pub reserve: Uint128, + pub denom: Denom, +} + +pub const TOKEN1: Item = Item::new("token1"); +pub const TOKEN2: Item = Item::new("token2"); + +pub const OWNER: Item> = Item::new("owner"); + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct Fees { + pub protocol_fee_recipient: Addr, + pub protocol_fee_percent: Decimal, + pub lp_fee_percent: Decimal, +} + +pub const FEES: Item = Item::new("fees"); + +pub const FROZEN: Item = Item::new("frozen"); + +``` +:: + +--------- +*Credits*: CosmWasm by example. +You can check the code on Github or open it with VS code. + +