Build a CW20 token factory
In this tutorial, you'll build a CW20 token factory. A token factory allows any CosmosSDK address (including a contract) to mint new fungible token.
Prerequisites
Use the following guides to set up your environment:
You'll also need:
- An IDE or text editor of your choice. This tutorial will use Visual Studio Code.
- A command line interface
Instantiate a new app using Terrain
Instantiate a new app using Terrain:
_1terrain new token_factory
When the app is generated, the following displays:
_4generating app token_factory:_4- workspace... done_4- contract... done_4- frontend... done
Generate the cw20_factory_token contract
a. Navigate to the token_factory directory:
_1cd token_factory
b. Instantiate the cw20_factory_token contract:
_1terrain contract:new cw20_factory_token
When the contract is generated, the following displays:
_2generating contract cw20_factory_token:_2- contract... done
Modify the mnemonics passphrase
Before editing the smart contracts you instantiated in step 2, modify the mnemonic you'll use to do deploy the contract to LocalTerra:
a. Open /keys.terrain.js and set the mnemonics to the following:
_1notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius
Because the wallet contains funds, it is recommended that you also import the passphrase listed below into the Fluid Station Extension. You can view other example mnemonics on Github:
_1notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius
The module.exports section of your keys.terrain.js file should now look similar to the following:
_6module.exports = {_6 test: {_6 mnemonic:_6 "notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius",_6 },_6};
Deploy the smart contracts
The token_factory contract mints new cw20_factory_token tokens, increases the supply of minted tokens, burns tokens to return LUNA and tracks all tokens created.
Deploy each smart contract to validate that the development environment is configured correctly:
a. Deploy the token_factory contract:
_1terrain deploy token_factory --signer test
b. Deploy the cw20_factory_token contract:
_1terrain deploy cw20_factory_token --signer test
Modify the CW20 Factory Token smart contract
In this section, you will modify the cw20_factory_token contract that you instantiated. This contract implements the CW20 Base, along with several custom files.
To modify the cw20_factory_token contract, follow the procedure below.
1. Add the the CW20 base
First, add the CW20 Base, which implements the base CW20 token functionalities. This allows:
- the smart contract to be easily deployed to LocalTerra
- extended functionality using the migration implementation.
To add the CW20 Base to the cw20_factory_token contract, do the following:
a. Navigate to /token_factory/contracts/cw20_factory_token/.
_1cd /token_factory/contracts/cw20_factory_token/
b. Open cargo.toml and add this to the dependencies:
_6# ..._6_6[dependencies]_6cw20-base = { version = "0.13.2", features = ["library"] }_6_6# ...
2. Modify the contract files
Now that you've added the CW20 Base to implement the base CW20 token logic, modify the following files:
msg.rslib.rscontract.rsschemas.rs
To modify the contract files, follow the procedure below.
a. Navigate to /token_factory/contracts/cw20_factory_token/src.
_1cd /token_factory/contracts/cw20_factory_token/src
b. Open msg.rs and paste the following:
_5use schemars::JsonSchema;_5use serde::{Deserialize, Serialize};_5_5#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_5pub struct MigrateMsg {}
c. Save and close msg.rs.
d. Open lib.rs and paste the following:
_2pub mod contract;_2pub mod msg;
e. Save and close lib.rs.
f. Open contract.rs and paste the following:
_116#[cfg(not(feature = "library"))]_116use cosmwasm_std::entry_point;_116use cosmwasm_std::{_116 to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult,_116};_116use cw20_base::ContractError;_116use cw20_base::enumerable::{query_all_allowances, query_all_accounts};_116use cw20_base::msg::{QueryMsg,ExecuteMsg};_116_116use crate::msg::MigrateMsg;_116use cw2::set_contract_version;_116use cw20_base::allowances::{_116 execute_decrease_allowance, execute_increase_allowance, execute_send_from,_116 execute_transfer_from, query_allowance, execute_burn_from,_116};_116use cw20_base::contract::{_116 execute_mint, execute_send, execute_transfer, execute_update_marketing,_116 execute_upload_logo, query_balance, query_token_info, query_minter, query_download_logo, query_marketing_info, execute_burn,_116};_116_116// version info for migration info_116const CONTRACT_NAME: &str = "crates.io:cw20_factory_token";_116const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");_116_116#[cfg_attr(not(feature = "library"), entry_point)]_116pub fn instantiate(_116 deps: DepsMut,_116 env: Env,_116 info: MessageInfo,_116 msg: cw20_base::msg::InstantiateMsg,_116) -> Result<Response, ContractError> {_116 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;_116_116 /* Execute the instantiate method from cw_20_base as the code from that_116 library is already battle tested we do not have to re-write the full_116 functionality: https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw20-base*/_116 Ok(cw20_base::contract::instantiate(deps, env, info, msg)?)_116}_116_116#[cfg_attr(not(feature = "library"), entry_point)]_116pub fn execute(_116 deps: DepsMut,_116 env: Env,_116 info: MessageInfo,_116 msg: ExecuteMsg,_116) -> Result<Response, cw20_base::ContractError> {_116 match msg {_116 ExecuteMsg::Transfer { recipient, amount } => {_116 execute_transfer(deps, env, info, recipient, amount)_116 }_116 ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount),_116 ExecuteMsg::Send {_116 contract,_116 amount,_116 msg,_116 } => execute_send(deps, env, info, contract, amount, msg),_116 ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount),_116 ExecuteMsg::IncreaseAllowance {_116 spender,_116 amount,_116 expires,_116 } => execute_increase_allowance(deps, env, info, spender, amount, expires),_116 ExecuteMsg::DecreaseAllowance {_116 spender,_116 amount,_116 expires,_116 } => execute_decrease_allowance(deps, env, info, spender, amount, expires),_116 ExecuteMsg::TransferFrom {_116 owner,_116 recipient,_116 amount,_116 } => execute_transfer_from(deps, env, info, owner, recipient, amount),_116 ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount),_116 ExecuteMsg::SendFrom {_116 owner,_116 contract,_116 amount,_116 msg,_116 } => execute_send_from(deps, env, info, owner, contract, amount, msg),_116 ExecuteMsg::UpdateMarketing {_116 project,_116 description,_116 marketing,_116 } => execute_update_marketing(deps, env, info, project, description, marketing),_116 ExecuteMsg::UploadLogo(logo) => execute_upload_logo(deps, env, info, logo),_116 }_116}_116_116#[cfg_attr(not(feature = "library"), entry_point)]_116pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {_116 match msg {_116 /* Default methods from CW20 Standard with no modifications:_116 https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw20-base */_116 QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?),_116 QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?),_116 QueryMsg::Minter {} => to_binary(&query_minter(deps)?),_116 QueryMsg::Allowance { owner, spender } => {_116 to_binary(&query_allowance(deps, owner, spender)?)_116 }_116 QueryMsg::AllAllowances {_116 owner,_116 start_after,_116 limit,_116 } => to_binary(&query_all_allowances(deps, owner, start_after, limit)?),_116 QueryMsg::AllAccounts { start_after, limit } => {_116 to_binary(&query_all_accounts(deps, start_after, limit)?)_116 }_116 QueryMsg::MarketingInfo {} => to_binary(&query_marketing_info(deps)?),_116 QueryMsg::DownloadLogo {} => to_binary(&query_download_logo(deps)?),_116 }_116}_116_116#[cfg_attr(not(feature = "library"), entry_point)]_116pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {_116 Ok(Response::default())_116}
g. Save and close contract.rs.
h. Open schemas.rs and paste the following:
_17use std::env::current_dir;_17use std::fs::create_dir_all;_17_17use cosmwasm_schema::{export_schema, remove_schemas, schema_for};_17use cw20_base::msg::{InstantiateMsg, QueryMsg, ExecuteMsg};_17_17_17fn main() {_17 let mut out_dir = current_dir().unwrap();_17 out_dir.push("schema");_17 create_dir_all(&out_dir).unwrap();_17 remove_schemas(&out_dir).unwrap();_17_17 export_schema(&schema_for!(InstantiateMsg), &out_dir);_17 export_schema(&schema_for!(ExecuteMsg), &out_dir);_17 export_schema(&schema_for!(QueryMsg), &out_dir);_17}
i. Close and save schemas.rs:
3. Generate and test the schema
a. Navigate to /token_factory/contracts/cw20_factory_token:
_1cd /token_factory/contracts/cw20_factory_token
b. Generate the new schema:
_1cargo schema
c. Test the schema:
_1cargo test
4. Modify terrain.config.json
a. Open terrain.config.json.
b. Modify the InstantiateMsg property in the terrain.config.json so that it contains the name, symbol, decimals and initial_balances shown below. This allows you to send the correct data to the smart contract upon instantiation:
_19{_19 "_global": {_19 "_base": {_19 "instantiation": {_19 "instantiateMsg": {_19 "name": "Bit Money",_19 "symbol": "BTM",_19 "decimals": 2,_19 "initial_balances": [_19 {_19 "amount": "123",_19 "address": "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"_19 }_19 ]_19 }_19 }_19 }_19 }, // ..._19}
5. Test the smart contract deployment
Deploy the contract again to confirm that the workplace still compiles.
_1terrain deploy cw20_factory_token --signer test
If your code is not working as expected, you can clone the repo with all the changes described above so that you can continue with the tutorial. To clone the repo, do the following:
_3git clone -n https://github.com/emidev98/token-factory_3cd token-factory_3git checkout 8da7892486704c54e33442b156d63178f5137527
6. Use crate.io to implement the CW20 Token Factory as a dependency
For the purpose of this tutorial, crates.io is used to implement the CW20 Token Factory as a dependency. This ensures that CW20 Token Factory is platform agnostic, so you can use Linux, Windows or Mac.
As the deployment to crates.io is out of scope for this tutorial, you can find the CW20 Token Factory package deployed to crates.io. You can use this deployment when you add the CW20 Token Factory contract as a dependency of the Token Factory contract in the the next section.
Create the Token Factory smart contract
To set up the contract, follow the procedure below:
1. Add the dependencies
In this section, you will add the following dependencies to cargo.toml:
cw2cw20cw20-basecw20_factory_token
To add the dependencies, do the following:
a. Navigate to /token_factory/contracts/token_factory.
b. Open cargo.toml and add the dependencies inside the header:
_7# ..._7[dependencies]_7cw2 = "0.13.2"_7cw20 = "0.13.2"_7cw20-base = { version = "0.13.2", features = ["library"] }_7cw20_factory_token = { version = "0.6.0", features = ["library"] }_7# ...
2. Modify the contract files
Now that you've added the dependencies, modify the following files:
error.rsmsg.rslib.rsstate.rscontract.rstest.rs
To modify the contract files, follow the procedure below:
a. Navigate to /token_factory/contracts/token_factory/src.
b. Open error.rs and add the following:
_37use cosmwasm_std::{StdError, Uint128};_37use thiserror::Error;_37_37#[derive(Error, Debug, PartialEq)]_37pub enum ContractError {_37 #[error("{0}")]_37 Std(#[from] StdError),_37_37 #[error("Unauthorized")]_37 Unauthorized {},_37_37 #[error("NotReceivedFunds")]_37 NotReceivedFunds {},_37_37 #[error("NotAllowZeroAmount")]_37 NotAllowZeroAmount {},_37_37_37 #[error("NotAllowedDenom")]_37 NotAllowedDenom {_37 denom: String_37 },_37_37_37 #[error("NotAllowedMultipleDenoms")]_37 NotAllowedMultipleDenoms {},_37_37 #[error("TokenAddressMustBeWhitelisted")]_37 TokenAddressMustBeWhitelisted {},_37_37 #[error("ReceivedFundsMismatchWithMintAmount")]_37 ReceivedFundsMismatchWithMintAmount {_37 received_amount: Uint128,_37 expected_amount: Uint128_37 },_37_37}
c. Close and save error.rs.
d. Open msg.rs and add the following:
_59use cosmwasm_std::Uint128;_59use schemars::JsonSchema;_59use serde::{Deserialize, Serialize};_59_59#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_59pub struct InstantiateMsg {_59 /* Denomination of the stable asset_59 https://docs.fluid.net/docs/develop/module-specifications/spec-market.html#market */_59 pub stable_denom: String,_59_59 /* Id of the contract uploaded for the first time to the chain_59 https://docs.fluid.net/docs/develop/module-specifications/spec-wasm.html#code-id */_59 pub token_contract_code_id: u64,_59}_59_59#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_59#[serde(rename_all = "snake_case")]_59pub enum ExecuteMsg {_59 /* Handle the deposits of native tokens into the smart contract to mint_59 the new pegged token 1:1 with LUNA or to increase circulation supply. */_59 Deposit(DepositType),_59_59 /* Handle burn of pegged tokens 1:1 with LUNA which are added to_59 MINTED_TOKENS list and return the LUNA stored into the contract. */_59 Burn {_59 amount: Uint128,_59 token_address: String,_59 },_59}_59_59#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_59#[serde(rename_all = "snake_case")]_59pub enum DepositType {_59 /* Instantiate a CW20_base token */_59 Instantiate(cw20_base::msg::InstantiateMsg),_59_59 /* Create new tokens based on token_address, amount of LUNA send to_59 this contract and recipient address */_59 Mint {_59 token_address: String,_59 recipient: String,_59 },_59}_59_59#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_59#[serde(rename_all = "snake_case")]_59pub enum QueryMsg {_59 /* Returns the list of token addresses that were created with this contract */_59 GetMintedTokens {},_59}_59_59#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_59#[serde(rename_all = "snake_case")]_59pub struct MintedTokens {_59 pub minted_tokens: Vec<String>,_59}_59_59#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_59pub struct MigrateMsg {}
e. Close and save msg.rs.
f. Open state.rs and add the following:
_18use schemars::JsonSchema;_18use serde::{Deserialize, Serialize};_18use cw_storage_plus::Item;_18_18#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]_18pub struct Config {_18 /* Denomination of the stable asset_18 https://docs.fluid.net/docs/develop/module-specifications/spec-market.html#market */_18 pub stable_denom: String,_18_18 /* Id of the contract uploaded to the chain used to instantiate_18 the different tokens_18 https://docs.fluid.net/docs/develop/module-specifications/spec-wasm.html#code-id */_18 pub token_contract_code_id: u64,_18}_18_18pub const CONFIG: Item<Config> = Item::new("config");_18pub const MINTED_TOKENS: Item<Vec<String>> = Item::new("minted_tokens");
g. Close and save state.rs.
h. Open lib.rs and add the following:
_6pub mod contract;_6pub mod msg;_6pub mod state;_6pub mod error;_6mod test;_6pub use crate::error::ContractError;
f. Open contract.rs and add the following:
_344use crate::error::ContractError;_344use crate::msg::{DepositType, ExecuteMsg, InstantiateMsg, MigrateMsg, MintedTokens, QueryMsg};_344use std::vec;_344use crate::state::{Config, CONFIG, MINTED_TOKENS};_344_344#[cfg(not(feature = "library"))]_344use cosmwasm_std::entry_point;_344use cosmwasm_std::{_344 coins, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply,_344 Response, StdError, StdResult, SubMsg, Uint128, WasmMsg,_344};_344use cw2::set_contract_version;_344_344/* Define contract name and version */_344_344const CONTRACT_NAME: &str = "crates.io:token_factory";_344const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");_344const INSTANTIATE_REPLY_ID: u64 = 1;_344_344#[cfg_attr(not(feature = "library"), entry_point)]_344pub fn instantiate(_344 deps: DepsMut,_344 _env: Env,_344 _info: MessageInfo,_344 msg: InstantiateMsg,_344) -> Result<Response, ContractError> {_344 /* Define the initial configuration for this contract that way you can_344 limit the type of coin you want to accept each time a token_factory is_344 created and also which kind of token would you like to mint based on_344 the code id of the contract deployed */_344 let state = Config {_344 stable_denom: msg.stable_denom.to_string(),_344 token_contract_code_id: msg.token_contract_code_id,_344 };_344_344 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;_344 CONFIG.save(deps.storage, &state)?;_344 MINTED_TOKENS.save(deps.storage, &Vec::new())?;_344_344 Ok(Response::new()_344 .add_attribute("method", "instantiate")_344 .add_attribute(_344 "token_contract_code_id",_344 msg.token_contract_code_id.to_string(),_344 ))_344}_344_344#[cfg_attr(not(feature = "library"), entry_point)]_344_344pub fn execute(_344 deps: DepsMut,_344 env: Env,_344 info: MessageInfo,_344 msg: ExecuteMsg,_344) -> Result<Response, ContractError> {_344 match msg {_344 /* Method executed each time someone send funds to the contract to mint_344 a new token or to increase already existent tokens circulating supply */_344 ExecuteMsg::Deposit(deposit_type) => match_deposit(deps.as_ref(), env, info, deposit_type),_344_344 /* Method used to burn an existent token created thru this contract_344 and send the LUNA back to the address that burn these tokens.*/_344 ExecuteMsg::Burn {_344 amount,_344 token_address,_344 } => execute_burn_from(deps, info, amount, token_address),_344 }_344}_344_344pub fn match_deposit(_344 deps: Deps,_344 env: Env,_344 info: MessageInfo,_344 deposit_type: DepositType,_344) -> Result<Response, ContractError> {_344 match deposit_type {_344 /* When the InstantiateMsg struct is send the factory will_344 execute this code and a new token with the defined properties_344 will be minted */_344 DepositType::Instantiate(token_data) => {_344 execute_instantiate_token(deps, env, info, token_data)_344 }_344_344 /* If a token_address and recipient is received along with a_344 deposit this method will increase the supply of an already_344 existent token by the defined units of LUNA received */_344 DepositType::Mint {_344 token_address,_344_344 recipient,_344 } => execute_mint(deps, info, token_address, recipient),_344 }_344}_344_344pub fn execute_instantiate_token(_344 deps: Deps,_344 env: Env,_344 info: MessageInfo,_344 mut token_data: cw20_base::msg::InstantiateMsg,_344) -> Result<Response, ContractError> {_344 let config = CONFIG.load(deps.storage)?;_344 let received_funds = get_received_funds(&deps, &info)?;_344 let mut expected_amount = Uint128::zero();_344_344 /* Add all initial token supply */_344 token_data_344 .initial_balances_344 .iter()_344 .for_each(|t| expected_amount += t.amount);_344_344 /* Check if received_funds is different than_344 initial token supply and throw an error */_344 if expected_amount.ne(&received_funds.amount) {_344 return Err(ContractError::ReceivedFundsMismatchWithMintAmount {_344 received_amount: received_funds.amount,_344_344 expected_amount,_344 });_344 }_344_344 /* If a minter exists replace the minter address with_344 the token_factory address that way the minting is only_344 allowed thru this smart contract. */_344 token_data.mint = match token_data.mint {_344 None => None,_344 Some(mut e) => {_344 e.minter = env.contract.address.to_string();_344_344 Some(e)_344 }_344 };_344_344 /* Create a WasmMsg to mint new CW20-base token._344 https://github.com/CosmWasm/cw-plus/tree/0.9.x/contracts/cw20-base */_344 let instantiate_message = WasmMsg::Instantiate {_344 admin: Some(env.contract.address.to_string()),_344 code_id: config.token_contract_code_id,_344 msg: to_binary(&token_data)?,_344 funds: vec![],_344 label: token_data.name,_344 };_344_344 /* Define the SubMessage on CosmWasm API to allow a callback on reply_344 entry point. This call will be executed with INSTANTIATE_REPLY_ID if_344 the call succeed after being executed by the method add_submessage(response)_344 from Response implementation._344 More Info: https://docs.cosmwasm.com/docs/1.0/smart-contracts/message/submessage */_344 let sub_msg = SubMsg::reply_on_success(instantiate_message, INSTANTIATE_REPLY_ID);_344_344 /* Respond with the method name and SubMsg._344 SubMsg will be executed to callback on reply_344 method with INSTANTIATE_REPLY_ID as identifier_344 to complete further operations */_344 Ok(Response::new()_344 .add_attribute("method", "instantiate_token")_344 .add_submessage(sub_msg))_344}_344_344pub fn get_received_funds(deps: &Deps, info: &MessageInfo) -> Result<Coin, ContractError> {_344 let config = CONFIG.load(deps.storage)?;_344_344 match info.funds.get(0) {_344 None => return Err(ContractError::NotReceivedFunds {}),_344 Some(received) => {_344 /* Amount of tokens received cannot be zero */_344 if received.amount.is_zero() {_344 return Err(ContractError::NotAllowZeroAmount {});_344 }_344 /* Allow to receive only token denomination defined_344 on contract instantiation "config.stable_denom" */_344 if received.denom.ne(&config.stable_denom) {_344 return Err(ContractError::NotAllowedDenom {_344 denom: received.denom.to_string(),_344 });_344 }_344_344 /* Only one token can be received */_344 if info.funds.len() > 1 {_344 return Err(ContractError::NotAllowedMultipleDenoms {});_344 }_344_344 Ok(received.clone())_344 }_344 }_344}_344_344pub fn execute_mint(_344 deps: Deps,_344 info: MessageInfo,_344 token_address: String,_344 recipient: String,_344) -> Result<Response, ContractError> {_344 let received_funds = get_received_funds(&deps, &info)?;_344_344 let token_addr_from_list = MINTED_TOKENS_344 .load(deps.storage)_344 .unwrap()_344 .into_iter()_344 .find(|t| t == &token_address);_344_344 /* Check if the token to be minted exists in the list, otherwise_344 throw an error because minting must not be allowed for a token_344 that was not created with this factory */_344 if token_addr_from_list == None {_344 return Err(ContractError::TokenAddressMustBeWhitelisted {});_344 }_344_344 /* Create an execute message to mint new units of an existent token */_344 let execute_mint = WasmMsg::Execute {_344 contract_addr: token_address.clone(),_344_344 msg: to_binary(&cw20_base::msg::ExecuteMsg::Mint {_344 amount: received_funds.amount,_344_344 recipient: recipient.clone(),_344 })?,_344_344 funds: vec![],_344 };_344_344 /* This type of SubMessage will never reply as no further operation is needed,_344 but for sure the mint call to instantiated cw20_base contract needs to be done._344 More Info: https://docs.cosmwasm.com/docs/1.0/smart-contracts/message/submessage */_344 let mint_sub_msg = SubMsg::new(execute_mint);_344_344 Ok(Response::new()_344 .add_attribute("method", "mint")_344 .add_submessage(mint_sub_msg))_344}_344_344pub fn execute_burn_from(_344 deps: DepsMut,_344 info: MessageInfo,_344 amount: Uint128,_344 token_address: String,_344) -> Result<Response, ContractError> {_344 let config = CONFIG.load(deps.storage)?;_344_344 let token_addr_from_list = MINTED_TOKENS_344 .load(deps.storage)_344 .unwrap()_344 .into_iter()_344 .find(|t| t == &token_address);_344_344 /* Check if the token to be burned exists in the list, otherwise_344 throw an error because minting must not be allowed for a token_344 that was not created thru the factory */_344 if token_addr_from_list == None {_344 return Err(ContractError::TokenAddressMustBeWhitelisted {});_344 }_344_344 /* Amount of tokens to be burned must not be zero */_344 if amount.is_zero() {_344 return Err(ContractError::NotAllowZeroAmount {});_344 }_344_344 /* Create a SubMessage to decrease the circulating supply of existent_344 CW20 Tokens from the token_address._344 https://github.com/CosmWasm/cosmwasm/blob/main/SEMANTICS.md#submessages */_344 let sub_msg_burn = SubMsg::new(WasmMsg::Execute {_344 contract_addr: token_address.clone(),_344 msg: to_binary(&cw20_base::msg::ExecuteMsg::BurnFrom {_344 owner: info.sender.to_string(),_344 amount,_344 })?,_344 funds: vec![],_344 });_344_344 /* Create a SubMessage to transfer fund from this smart contract to_344 the address that burns the CW20 Tokens*/_344 let sub_msg_send = SubMsg::new(CosmosMsg::Bank(BankMsg::Send {_344 to_address: info.sender.to_string(),_344_344 amount: coins(amount.u128(), config.stable_denom),_344 }));_344_344 Ok(Response::new()_344 .add_attribute("method", "burn")_344 .add_submessages(vec![sub_msg_burn, sub_msg_send]))_344}_344_344/* In order to handle any callback from previous SubMessages "reply"_344function must be implemented and iterate over "msg.id" to allow_344the completion of the callback.*/_344#[cfg_attr(not(feature = "library"), entry_point)]_344pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> StdResult<Response> {_344 match msg.id {_344 INSTANTIATE_REPLY_ID => handle_instantiate_reply(deps, msg),_344_344 id => Err(StdError::generic_err(format!("Unknown reply id: {}", id))),_344 }_344}_344_344fn handle_instantiate_reply(deps: DepsMut, msg: Reply) -> StdResult<Response> {_344 let result = msg.result.into_result().map_err(StdError::generic_err)?;_344_344 /* Find the event type instantiate which contains the contract_address*/_344 let event = result_344 .events_344 .iter()_344 .find(|event| event.ty == "instantiate")_344 .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?;_344_344 /* Find the _contract_address from instantiate event*/_344 let contract_address = &event_344 .attributes_344 .iter()_344 .find(|attr| attr.key == "_contract_address")_344 .ok_or_else(|| StdError::generic_err("cannot find `_contract_address` attribute"))?_344 .value;_344_344 /* Update the state of the contract adding the new generated MINTED_TOKEN */_344 MINTED_TOKENS.update(deps.storage, |mut tokens| -> StdResult<Vec<String>> {_344 tokens.push(contract_address.to_string());_344_344 Ok(tokens)_344 })?;_344_344 Ok(Response::new()_344 .add_attribute("method", "handle_instantiate_reply")_344 .add_attribute("contract_address", contract_address))_344}_344_344#[cfg_attr(not(feature = "library"), entry_point)]_344pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {_344 match msg {_344 /* Return the list of all tokens that were minted thru this contract */_344 QueryMsg::GetMintedTokens {} => to_binary(&query_minted_tokens(deps)?),_344 }_344}_344_344fn query_minted_tokens(deps: Deps) -> StdResult<MintedTokens> {_344 Ok(MintedTokens {_344 minted_tokens: MINTED_TOKENS.load(deps.storage)?,_344 })_344}_344_344/* In case you want to upgrade this contract you can find information about_344how to migrate the contract in the following link:_344https://docs.fluid.net/docs/develop/dapp/quick-start/contract-migration.html*/_344#[cfg_attr(not(feature = "library"), entry_point)]_344pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {_344 Ok(Response::default())_344}
i. Close and save lib.rs.
j. Open test.rs and add the following:
_261#[cfg(test)]_261mod tests {_261 use crate::{_261 contract::{execute, instantiate, query, reply},_261 msg::{DepositType, ExecuteMsg, InstantiateMsg, MintedTokens, QueryMsg},_261 };_261 use cosmwasm_std::{_261 coins, from_binary,_261 testing::{mock_dependencies, mock_env, mock_info},_261 to_binary, Attribute, BankMsg, Coin, CosmosMsg, DepsMut, Event, Reply, Response, SubMsg,_261 SubMsgResponse, Uint128, WasmMsg,_261 };_261 use cw20::{Cw20Coin, MinterResponse};_261_261 #[test]_261 fn test_instantiate() {_261 // GIVEN_261 let mut deps = mock_dependencies();_261_261 // WHEN_261 let res = do_instantiate(deps.as_mut());_261_261 // THEN_261 let attrs = res.attributes;_261 assert_eq!(_261 vec![_261 Attribute {_261 key: "method".to_string(),_261 value: "instantiate".to_string()_261 },_261 Attribute {_261 key: "token_contract_code_id".to_string(),_261 value: "1".to_string()_261 }_261 ],_261 attrs_261 );_261 }_261_261 #[test]_261 fn test_mint_token() {_261 // GIVEN_261 let mut deps = mock_dependencies();_261_261 // WHEN_261 do_instantiate(deps.as_mut());_261 let res = do_mint_new_token(deps.as_mut());_261_261 // THEN_261 let res_attr = res.attributes;_261 assert_eq!(1, res_attr.len());_261 assert_eq!("instantiate_token", res_attr.get(0).unwrap().value);_261_261 let res_message = res.messages;_261 assert_eq!(1, res_message.len());_261 let success_reply = SubMsg::reply_on_success(_261 CosmosMsg::Wasm(WasmMsg::Instantiate {_261 admin: Some("cosmos2contract".to_string()),_261 code_id: 1,_261 funds: vec![],_261 msg: to_binary(&cw20_base::msg::InstantiateMsg {_261 name: "Bit Money".to_string(),_261 symbol: "BTM".to_string(),_261 decimals: 2,_261 mint: Some(MinterResponse {_261 minter: "cosmos2contract".to_string(),_261 cap: Some(Uint128::new(1234)),_261 }),_261 initial_balances: vec![Cw20Coin {_261 amount: Uint128::new(123),_261 address: "creator".to_string(),_261 }],_261 marketing: None,_261 })_261 .unwrap(),_261 label: "Bit Money".to_string(),_261 }),_261 1,_261 );_261 assert_eq!(&success_reply, res_message.get(0).unwrap());_261 }_261_261 #[test]_261 fn test_reply_instantiate_event() {_261 // GIVEN_261 let mut deps = mock_dependencies();_261 let env = mock_env();_261 let query_minted_tokens = QueryMsg::GetMintedTokens {};_261_261 // WHEN_261 do_instantiate(deps.as_mut());_261 do_mint_new_token(deps.as_mut());_261 let do_instantiate_res = do_reply_instantiate_event(deps.as_mut());_261 let query_res = query(deps.as_ref(), env, query_minted_tokens).unwrap();_261 let query_res: MintedTokens = from_binary(&query_res).unwrap();_261_261 // THEN_261 assert_eq!(_261 Response::new()_261 .add_attribute("method", "handle_instantiate_reply")_261 .add_attribute("contract_address", "bit_money_contract_address"),_261 do_instantiate_res_261 );_261 assert_eq!(_261 MintedTokens {_261 minted_tokens: vec!["bit_money_contract_address".to_string()]_261 },_261 query_res_261 );_261 }_261_261 #[test]_261 fn test_mint_existent_token() {_261 // GIVEN_261 let mut deps = mock_dependencies();_261 let env = mock_env();_261 let info = mock_info(_261 "creator",_261 &vec![Coin {_261 denom: "uluna".to_string(),_261 amount: Uint128::new(1),_261 }],_261 );_261 let msg = ExecuteMsg::Deposit(DepositType::Mint {_261 token_address: "bit_money_contract_address".to_string(),_261 recipient: "creator".to_string(),_261 });_261_261 // WHEN_261 do_instantiate(deps.as_mut());_261 do_mint_new_token(deps.as_mut());_261 do_reply_instantiate_event(deps.as_mut());_261 let execute_res = execute(deps.as_mut(), env, info, msg).unwrap();_261_261 // THEN_261 assert_eq!(_261 Response::new()_261 .add_attribute("method", "mint")_261 .add_messages(vec![CosmosMsg::Wasm(WasmMsg::Execute {_261 contract_addr: "bit_money_contract_address".to_string(),_261 msg: to_binary(&cw20_base::msg::ExecuteMsg::Mint {_261 amount: Uint128::new(1),_261 recipient: "creator".to_string()_261 })_261 .unwrap(),_261 funds: vec![],_261 })]),_261 execute_res_261 );_261 }_261_261 #[test]_261 fn test_burn_tokens() {_261 // GIVEN_261 let mut deps = mock_dependencies();_261 let env = mock_env();_261 let info = mock_info("creator", &[]);_261 let exec_burn_tokens = ExecuteMsg::Burn {_261 amount: Uint128::new(123),_261 token_address: "bit_money_contract_address".to_string(),_261 };_261_261 // WHEN_261 do_instantiate(deps.as_mut());_261 do_reply_instantiate_event(deps.as_mut());_261 do_mint_new_token(deps.as_mut());_261_261 let res = execute(deps.as_mut(), env, info, exec_burn_tokens).unwrap();_261_261 // THEN_261 assert_eq!(1, res.attributes.len());_261 assert_eq!("burn", res.attributes.get(0).unwrap().value);_261 assert_eq!(2, res.messages.len());_261 assert_eq!(_261 vec![_261 SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute {_261 contract_addr: "bit_money_contract_address".to_string(),_261 msg: to_binary(&cw20_base::msg::ExecuteMsg::BurnFrom {_261 owner: "creator".to_string(),_261 amount: Uint128::new(123),_261 })_261 .unwrap(),_261 funds: vec![],_261 })),_261 SubMsg::new(CosmosMsg::Bank(BankMsg::Send {_261 to_address: "creator".to_string(),_261 amount: coins(123 as u128, "uluna")_261 }))_261 ],_261 res.messages_261 );_261 }_261_261 /*_261 * HELPER METHODS TO DO NOT REPEAT CODE MANY TIMES_261 */_261_261 fn do_instantiate(deps: DepsMut) -> Response {_261 let instantiate_msg = InstantiateMsg {_261 stable_denom: "uluna".to_string(),_261 token_contract_code_id: 1,_261 };_261 let info = mock_info("creator", &[]);_261 let env = mock_env();_261_261 instantiate(deps, env, info, instantiate_msg).unwrap()_261 }_261_261 fn do_mint_new_token(deps: DepsMut) -> Response {_261 let env = mock_env();_261 let info = mock_info(_261 "i_am_the_sender",_261 &vec![Coin {_261 denom: "uluna".to_string(),_261 amount: Uint128::new(123),_261 }],_261 );_261 let token_msg = cw20_base::msg::InstantiateMsg {_261 name: "Bit Money".to_string(),_261 symbol: "BTM".to_string(),_261 decimals: 2,_261 mint: Some(MinterResponse {_261 minter: "creator".to_string(),_261 cap: Some(Uint128::new(1234)),_261 }),_261 initial_balances: vec![Cw20Coin {_261 amount: Uint128::new(123),_261 address: "creator".to_string(),_261 }],_261 marketing: None,_261 };_261 let msg = ExecuteMsg::Deposit(DepositType::Instantiate(token_msg.clone()));_261_261 execute(deps, env.clone(), info.clone(), msg).unwrap()_261 }_261_261 /* Confirm reply event form instantiate method. That way_261 the minted_tokens addresses can be whitelisted in factory.*/_261 fn do_reply_instantiate_event(deps: DepsMut) -> Response {_261 let env = mock_env();_261_261 let event = Event::new("instantiate_contract")_261 .add_attribute("creator", "token_factory_addr")_261 .add_attribute("admin", "i_am_the_sender")_261 .add_attribute("code_id", "1")_261 .add_attribute("contract_address", "bit_money_contract_address");_261_261 reply(_261 deps,_261 env,_261 Reply {_261 id: 1,_261 result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {_261 events: vec![event],_261 data: None,_261 }),_261 },_261 )_261 .unwrap()_261 }_261}
h. Navigate to token_factory/contracts/token_factory/examples.
i. Open schema.rs.
_16use std::env::current_dir;_16use std::fs::create_dir_all;_16use cosmwasm_schema::{export_schema, remove_schemas, schema_for};_16use token_factory::msg::{ExecuteMsg, QueryMsg};_16_16fn main() {_16 let mut out_dir = current_dir().unwrap();_16 out_dir.push("schema");_16 create_dir_all(&out_dir).unwrap();_16_16 remove_schemas(&out_dir).unwrap();_16_16 export_schema(&schema_for!(token_factory::msg::InstantiateMsg), &out_dir);_16 export_schema(&schema_for!(ExecuteMsg), &out_dir);_16 export_schema(&schema_for!(QueryMsg), &out_dir);_16}
3. Generate and test the schema
Now you have modified the token_factory contract, generate the schema and run the tests to validate that the code works as expected:
a. Navigate to /token_factory/contracts/token_factory.
_1cd /token_factory/contracts/token_factory
b. Generate the schema:
_1 cargo schema
You should see output similar to:
_8Finished dev [unoptimized + debuginfo] target(s) in 0.02s_8Running `target/debug/examples/schema`_8Removing "~/Documents/github/token_factory/contracts/token_factory/schema/execute_msg.json" …_8Removing "~/Documents/github/token_factory/contracts/token_factory/schema/instantiate_msg.json" …_8Removing "~/Documents/github/token_factory/contracts/token_factory/schema/query_msg.json" …_8Created ~/Documents/github/token_factory/contracts/token_factory/schema/instantiate_msg.json_8Created ~/Documents/github/token_factory/contracts/token_factory/schema/execute_msg.json_8Created ~/Documents/github/token_factory/contracts/token_factory/schema/query_msg.json
c. Run the tests:
_1cargo test
You will see output similar to the following:
_10Finished test [unoptimized + debuginfo] target(s) in 0.02s_10Running unittests (target/debug/deps/token_factory-03f77bf897cd72b7)_10_10running 5 tests_10test test::tests::test_instantiate ... ok_10test test::tests::test_burn_tokens ... ok_10test test::tests::test_mint_token ... ok_10test test::tests::test_mint_existent_token ... ok_10test test::tests::test_reply_instantiate_event ... ok_10test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
4. Modify terrain.config.json
a. Open terrain.config.json.
b. Modify the property InstantiateMsg, using your <token_contract_code_id>. The <token_contract_code_id> should not be surrounded by quotes:
To determine which <token_contract_code_id>, check the file refs.terrain.json from the workspace's root under the cw20-token_factory object.
_12{_12 "_global": {_12 "_base": {_12 "instantiation": {_12 "instantiateMsg": {_12 "stable_denom": "uluna",_12 "token_contract_code_id": <token_contract_id>_12 }_12 }_12 }_12 }, // ..._12}
Deploy the smart contract to LocalTerra
Now that you've created, modified and tested each smart contract, deploy the token_factory to your LocalTerra instance using Terrain:
_1terrain deploy token_factory --signer test
If your code is not working as expected, you can clone the repo with all changes done until now.
A hosted website for the token factory can be found here.