Build a CW20 tokens 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 tokens based. To accomplish this you’ll do the following:

Prerequisites#

First, set up your environment:

1. Instantiate a new app using Terrain#

a. Instantiate a new app using Terrain:

terrain new token-factory

When the app is generated, the following displays:

generating:
- contract... done
- frontend... done

b. Navigate to the contracts folder.

cd contracts/

c. Terrain automatically generates a sample counter contract in the contracts folder. Delete the counter smart contract folder to ensure a clean workspace:

rm -r counter/

2. Instantiate the token-factory and cw20-factory-token contracts#

a. Navigate to the token-factory directory:

cd token-factory

b. Instantiate the token-factory contract:

terrain code:new token-factory

When the contract is generated, the following displays:

generating contract... done

c. Instantiate the cw20-factory-token contract:

terrain code:new cw20-factory-token

When the contract is generated, the following displays:

generating contract... done

3. 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. Navigate to /token-factory

cd /token-factory

b. Open /keys.terrain.js and set the mnemonics to the following:

notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius

Tip

Because the wallet contains funds, it is recommended that you also import the passphrase listed below into the Terra Station Extension. You can view other example mnemonics [on Github] (https://github.com/terra-money/LocalTerra/blob/main/terracore/mnemonics.json#L9):

notice 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:

module.exports = {
  test: {
    mnemonic:
      "notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius",
  },
};

4. Deploy the smart contracts#

The token-factory contract mints new cw20-factory-token tokens, increases the supply of minted tokens, burns tokens to return UST and tracks all tokens created.

Deploy each smart contract to validate that the development environment is configured correctly:

a. Deploy the token-factory contract:

terrain deploy token-factory --signer test

b. Deploy the cw20-factory-token contract:

terrain deploy cw20-factory-token --signer test

5. 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:

To add the CW20 Base to the cw20-factory-token contract, do the following:

a. Navigate to /token-factory/contracts/cw20-factory-token/.

cd /token-factory/contracts/cw20-factory-token/

b. Open cargo.toml and add this to the dependencies:

# ...

[dependencies]
cw20-base = {  version = "0.8.1", features = ["library"] }

# ...

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.rs

  • lib.rs

  • contract.rs

  • schemas.rs

To modify the contract files, follow the procedure below.

a. Navigate to /token-factory/contracts/cw20-factory-token/src.

cd /token-factory/contracts/cw20-factory-token/src

b. Open msg.rs and paste the following:

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MigrateMsg {}

c. Save and close msg.rs.

d. Open lib.rs and paste the following:

pub mod contract;
pub mod msg;

e. Save and close lib.rs.

f. Open contract.rs and paste the following:

#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
    to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
};
use cw20_base::ContractError;
use cw20_base::enumerable::{query_all_allowances, query_all_accounts};
use cw20_base::msg::{QueryMsg,ExecuteMsg};

use crate::msg::MigrateMsg;
use cw2::set_contract_version;
use cw20_base::allowances::{
    execute_decrease_allowance, execute_increase_allowance, execute_send_from,
    execute_transfer_from, query_allowance, execute_burn_from,
};
use cw20_base::contract::{
    execute_mint, execute_send, execute_transfer, execute_update_marketing,
    execute_upload_logo, query_balance, query_token_info, query_minter, query_download_logo, query_marketing_info, execute_burn,
};

// version info for migration info
const CONTRACT_NAME: &str = "crates.io:cw20-factory-token";
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: cw20_base::msg::InstantiateMsg,
) -> Result<Response, ContractError> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    /* Execute the instantiate method from cw_20_base as the code from that
    library is already battle tested we do not have to re-write the full
    functionality: https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw20-base*/
    Ok(cw20_base::contract::instantiate(deps, env, info, msg)?)
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, cw20_base::ContractError> {
    match msg {
        ExecuteMsg::Transfer { recipient, amount } => {
            execute_transfer(deps, env, info, recipient, amount)
        }
        ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount),
        ExecuteMsg::Send {
            contract,
            amount,
            msg,
        } => execute_send(deps, env, info, contract, amount, msg),
        ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount),
        ExecuteMsg::IncreaseAllowance {
            spender,
            amount,
            expires,
        } => execute_increase_allowance(deps, env, info, spender, amount, expires),
        ExecuteMsg::DecreaseAllowance {
            spender,
            amount,
            expires,
        } => execute_decrease_allowance(deps, env, info, spender, amount, expires),
        ExecuteMsg::TransferFrom {
            owner,
            recipient,
            amount,
        } => execute_transfer_from(deps, env, info, owner, recipient, amount),
        ExecuteMsg::BurnFrom { owner, amount } => execute_burn_from(deps, env, info, owner, amount),
        ExecuteMsg::SendFrom {
            owner,
            contract,
            amount,
            msg,
        } => execute_send_from(deps, env, info, owner, contract, amount, msg),
        ExecuteMsg::UpdateMarketing {
            project,
            description,
            marketing,
        } => execute_update_marketing(deps, env, info, project, description, marketing),
        ExecuteMsg::UploadLogo(logo) => execute_upload_logo(deps, env, info, logo),
    }
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        /* Default methods from CW20 Standard with no modifications:
        https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw20-base */
        QueryMsg::Balance { address } => to_binary(&query_balance(deps, address)?),
        QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps)?),
        QueryMsg::Minter {} => to_binary(&query_minter(deps)?),
        QueryMsg::Allowance { owner, spender } => {
            to_binary(&query_allowance(deps, owner, spender)?)
        }
        QueryMsg::AllAllowances {
            owner,
            start_after,
            limit,
        } => to_binary(&query_all_allowances(deps, owner, start_after, limit)?),
        QueryMsg::AllAccounts { start_after, limit } => {
            to_binary(&query_all_accounts(deps, start_after, limit)?)
        }
        QueryMsg::MarketingInfo {} => to_binary(&query_marketing_info(deps)?),
        QueryMsg::DownloadLogo {} => to_binary(&query_download_logo(deps)?),
    }
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
    Ok(Response::default())
}

g. Save and close contract.rs.

h. Open schemas.rs and paste the following:

use std::env::current_dir;
use std::fs::create_dir_all;

use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
use cw20_base::msg::{InstantiateMsg, QueryMsg, ExecuteMsg};


fn main() {
    let mut out_dir = current_dir().unwrap();
    out_dir.push("schema");
    create_dir_all(&out_dir).unwrap();
    remove_schemas(&out_dir).unwrap();

    export_schema(&schema_for!(InstantiateMsg), &out_dir);
    export_schema(&schema_for!(ExecuteMsg), &out_dir);
    export_schema(&schema_for!(QueryMsg), &out_dir);
}

i. Close and save schemas.rs:

3. Generate and test the schema#

a. Navigate to /token-factory/contracts/cw20-factory-token:

cd /token-factory/contracts/cw20-factory-token

b. Generate the new schema:

cargo schema

c. Test the schema:

cargo 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:

{
  "_global": {
    "_base": {
      "instantiation": {
        "instantiateMsg": {
          "name": "Bit Money",
          "symbol": "BTM",
          "decimals": 2,
          "initial_balances": [
            {
              "amount": "123",
              "address": "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"
            }
          ]
        }
      }
    }
  }, // ...
}

5. Test the smart contract deployment#

Deploy the contract again to confirm that the workplace still compiles.

terrain deploy cw20-factory-token --signer test

Tip

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:

git clone -n https://github.com/emidev98/token-factory
cd token-factory
git checkout fdba3c89c464860fe8cd9aa17f1344d82d613522

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.

3. 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:

  • cw2

  • cw20

  • cw20-base

  • cw20-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:

# ...
[dependencies]
cw2 = "0.8.1"
cw20 = "0.8.1"
cw20-base = { version = "0.8.1", features = ["library"] }
cw20-factory-token = { version = "0.5.0", features = ["library"] }

# ...

2. Modify the contract files#

Now that you’ve added the dependencies, modify the following files:

  • error.rs

  • msg.rs

  • lib.rs

  • state.rs

  • contract.rs

  • test.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:

use cosmwasm_std::{StdError, Uint128};
use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")]
    Std(#[from] StdError),

    #[error("Unauthorized")]
    Unauthorized {},

    #[error("NotReceivedFunds")]
    NotReceivedFunds {},

    #[error("NotAllowZeroAmount")]
    NotAllowZeroAmount {},


    #[error("NotAllowedDenom")]
    NotAllowedDenom {
        denom: String
    },


    #[error("NotAllowedMultipleDenoms")]
    NotAllowedMultipleDenoms {},

    #[error("TokenAddressMustBeWhitelisted")]
    TokenAddressMustBeWhitelisted {},

    #[error("ReceivedFundsMismatchWithMintAmount")]
    ReceivedFundsMismatchWithMintAmount {
        received_amount: Uint128,
        expected_amount: Uint128
    },

}

c. Close and save error.rs.

d. Open msg.rs and add the following:

use cosmwasm_std::Uint128;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    /* Denomination of the stable asset
    https://docs.terra.money/docs/develop/module-specifications/spec-market.html#market */
    pub stable_denom: String,

    /* Id of the contract uploaded for the first time to the chain
    https://docs.terra.money/docs/develop/module-specifications/spec-wasm.html#code-id */
    pub token_contract_code_id: u64,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    /* Handle the deposits of native tokens into the smart contract to mint
    the new pegged token 1:1 with UST or to increase circulation supply. */
    Deposit(DepositType),

    /* Handle burn of pegged tokens 1:1 with UST which are added to
    MINTED_TOKENS list and return the UST stored into the contract. */
    Burn {
        amount: Uint128,
        token_address: String,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum DepositType {
    /* Instantiate a CW20_base token */
    Instantiate(cw20_base::msg::InstantiateMsg),

    /* Create new tokens based on token_address, amount of UST send to
    this contract and recipient address */
    Mint {
        token_address: String,
        recipient: String,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    /* Returns the list of token addresses that were created with this contract */
    GetMintedTokens {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct MintedTokens {
    pub minted_tokens: Vec<String>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MigrateMsg {}

e. Close and save msg.rs.

f. Open state.rs and add the following:

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cw_storage_plus::Item;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
    /* Denomination of the stable asset
    https://docs.terra.money/docs/develop/module-specifications/spec-market.html#market */
    pub stable_denom: String,

    /* Id of the contract uploaded to the chain used to instantiate
    the different tokens
    https://docs.terra.money/docs/develop/module-specifications/spec-wasm.html#code-id */
    pub token_contract_code_id: u64,
}

pub const CONFIG: Item<Config> = Item::new("config");
pub const MINTED_TOKENS: Item<Vec<String>> = Item::new("minted_tokens");

g. Close and save state.rs.

h. Open lib.rs and add the following:

pub mod contract;
pub mod msg;
pub mod state;
pub mod error;
mod test;
pub use crate::error::ContractError;

f. Open contract.rs and add the following:

use crate::error::ContractError;
use crate::msg::{DepositType, ExecuteMsg, InstantiateMsg, MigrateMsg, MintedTokens, QueryMsg};
use std::vec;
use crate::state::{Config, CONFIG, MINTED_TOKENS};

#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
    coins, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply,
    Response, StdError, StdResult, SubMsg, Uint128, WasmMsg,
};
use cw2::set_contract_version;

/* Define contract name and version */

const CONTRACT_NAME: &str = "crates.io:token-factory";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
const INSTANTIATE_REPLY_ID: u64 = 1;

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> Result<Response, ContractError> {
    /* Define the initial configuration for this contract that way you can
    limit the type of coin you want to accept each time a token-factory is
    created and also which kind of token would you like to mint based on
    the code id of the contract deployed */
    let state = Config {
        stable_denom: msg.stable_denom.to_string(),

        token_contract_code_id: msg.token_contract_code_id,
    };

    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    CONFIG.save(deps.storage, &state)?;
    MINTED_TOKENS.save(deps.storage, &Vec::new())?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute(
            "token_contract_code_id",
            msg.token_contract_code_id.to_string(),
        ))
}

#[cfg_attr(not(feature = "library"), entry_point)]

pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        /* Method executed each time someone send funds to the contract to mint
        a new token or to increase already existent tokens circulating supply */
        ExecuteMsg::Deposit(deposit_type) => match_deposit(deps.as_ref(), env, info, deposit_type),

        /* Method used to burn an existent token created thru this contract
        and send the UST back to the address that burn these tokens.*/
        ExecuteMsg::Burn {
            amount,
            token_address,
        } => execute_burn_from(deps, info, amount, token_address),
    }
}

pub fn match_deposit(
    deps: Deps,
    env: Env,
    info: MessageInfo,
    deposit_type: DepositType,
) -> Result<Response, ContractError> {
    match deposit_type {
        /* When the InstantiateMsg struct is send the factory will
        execute this code and a new token with the defined properties
        will be minted */
        DepositType::Instantiate(token_data) => {
            execute_instantiate_token(deps, env, info, token_data)
        }

        /* If a token_address and recipient is received along with a
        deposit this method will increase the supply of an already
        existent token by the defined units of UST received */
        DepositType::Mint {
            token_address,

            recipient,
        } => execute_mint(deps, info, token_address, recipient),
    }
}

pub fn execute_instantiate_token(
    deps: Deps,
    env: Env,
    info: MessageInfo,
    mut token_data: cw20_base::msg::InstantiateMsg,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let received_funds = get_received_funds(&deps, &info)?;
    let mut expected_amount = Uint128::zero();

    /* Add all initial token supply */
    token_data
        .initial_balances
        .iter()
        .for_each(|t| expected_amount += t.amount);

    /* Check if received_funds is different than
    initial token supply and throw an error */
    if expected_amount.ne(&received_funds.amount) {
        return Err(ContractError::ReceivedFundsMismatchWithMintAmount {
            received_amount: received_funds.amount,

            expected_amount,
        });
    }

    /* If a minter exists replace the minter address with
    the token-factory address that way the minting is only
    allowed thru this smart contract. */
    token_data.mint = match token_data.mint {
        None => None,

        Some(mut e) => {
            e.minter = env.contract.address.to_string();

            Some(e)
        }
    };

    /* Create a WasmMsg to mint new CW20-base token.
    https://github.com/CosmWasm/cw-plus/tree/0.9.x/contracts/cw20-base */
    let instantiate_message = WasmMsg::Instantiate {
        admin: Some(env.contract.address.to_string()),

        code_id: config.token_contract_code_id,

        msg: to_binary(&token_data)?,

        funds: vec![],

        label: token_data.name,
    };

    /* Define the SubMessage on CosmWasm API to allow a callback on reply
    entry point. This call will be executed with INSTANTIATE_REPLY_ID if
    the call succeed after being executed by the method add_submessage(response)
    from Response implementation.
    More Info: https://docs.cosmwasm.com/docs/1.0/smart-contracts/message/submessage */
    let sub_msg = SubMsg::reply_on_success(instantiate_message, INSTANTIATE_REPLY_ID);

    /* Respond with the method name and SubMsg.
    SubMsg will be executed to callback on reply
    method with INSTANTIATE_REPLY_ID as identifier
    to complete further operations */
    Ok(Response::new()
        .add_attribute("method", "instantiate_token")
        .add_submessage(sub_msg))
}

pub fn get_received_funds(deps: &Deps, info: &MessageInfo) -> Result<Coin, ContractError> {
    let config = CONFIG.load(deps.storage)?;

    match info.funds.get(0) {
        None => return Err(ContractError::NotReceivedFunds {}),
        Some(received) => {
            /* Amount of tokens received cannot be zero */
            if received.amount.is_zero() {
                return Err(ContractError::NotAllowZeroAmount {});
            }
            /* Allow to receive only token denomination defined
            on contract instantiation "config.stable_denom" */
            if received.denom.ne(&config.stable_denom) {
                return Err(ContractError::NotAllowedDenom {
                    denom: received.denom.to_string(),
                });
            }

            /* Only one token can be received */
            if info.funds.len() > 1 {
                return Err(ContractError::NotAllowedMultipleDenoms {});
            }

            Ok(received.clone())
        }
    }
}

pub fn execute_mint(
    deps: Deps,
    info: MessageInfo,
    token_address: String,
    recipient: String,
) -> Result<Response, ContractError> {
    let received_funds = get_received_funds(&deps, &info)?;

    let token_addr_from_list = MINTED_TOKENS
        .load(deps.storage)
        .unwrap()
        .into_iter()
        .find(|t| t == &token_address);

    /* Check if the token to be minted exists in the list, otherwise
    throw an error because minting must not be allowed for a token
    that was not created with this factory */
    if token_addr_from_list == None {
        return Err(ContractError::TokenAddressMustBeWhitelisted {});
    }

    /* Create an execute message to mint new units of an existent token */
    let execute_mint = WasmMsg::Execute {
        contract_addr: token_address.clone(),

        msg: to_binary(&cw20_base::msg::ExecuteMsg::Mint {
            amount: received_funds.amount,

            recipient: recipient.clone(),
        })?,

        funds: vec![],
    };

    /* This type of SubMessage will never reply as no further operation is needed,
    but for sure the mint call to instantiated cw20_base contract needs to be done.
    More Info: https://docs.cosmwasm.com/docs/1.0/smart-contracts/message/submessage */
    let mint_sub_msg = SubMsg::new(execute_mint);

    Ok(Response::new()
        .add_attribute("method", "mint")
        .add_submessage(mint_sub_msg))
}

pub fn execute_burn_from(
    deps: DepsMut,
    info: MessageInfo,
    amount: Uint128,
    token_address: String,
) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;

    let token_addr_from_list = MINTED_TOKENS
        .load(deps.storage)
        .unwrap()
        .into_iter()
        .find(|t| t == &token_address);

    /* Check if the token to be burned exists in the list, otherwise
    throw an error because minting must not be allowed for a token
    that was not created thru the factory */
    if token_addr_from_list == None {
        return Err(ContractError::TokenAddressMustBeWhitelisted {});
    }

    /* Amount of tokens to be burned must not be zero */
    if amount.is_zero() {
        return Err(ContractError::NotAllowZeroAmount {});
    }

    /* Create a SubMessage to decrease the circulating supply of existent
    CW20 Tokens from the token_address.
    https://github.com/CosmWasm/cosmwasm/blob/main/SEMANTICS.md#submessages */
    let sub_msg_burn = SubMsg::new(WasmMsg::Execute {
        contract_addr: token_address.clone(),
        msg: to_binary(&cw20_base::msg::ExecuteMsg::BurnFrom {
            owner: info.sender.to_string(),
            amount,
        })?,
        funds: vec![],
    });

    /* Create a SubMessage to transfer fund from this smart contract to
    the address that burns the CW20 Tokens*/
    let sub_msg_send = SubMsg::new(CosmosMsg::Bank(BankMsg::Send {
        to_address: info.sender.to_string(),

        amount: coins(amount.u128(), config.stable_denom),
    }));

    Ok(Response::new()
        .add_attribute("method", "burn")
        .add_submessages(vec![sub_msg_burn, sub_msg_send]))
}

/* In order to handle any callback from previous SubMessages "reply"
function must be implemented and iterate over "msg.id" to allow
the completion of the callback.*/
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> StdResult<Response> {
    match msg.id {
        INSTANTIATE_REPLY_ID => handle_instantiate_reply(deps, msg),

        id => Err(StdError::generic_err(format!("Unknown reply id: {}", id))),
    }
}

fn handle_instantiate_reply(deps: DepsMut, msg: Reply) -> StdResult<Response> {
    let result = msg.result.into_result().map_err(StdError::generic_err)?;

    /* Find the event type instantiate_contract which contains the contract_address*/
    let event = result
        .events
        .iter()
        .find(|event| event.ty == "instantiate_contract")
        .ok_or_else(|| StdError::generic_err("cannot find `instantiate_contract` event"))?;

    /* Find the contract_address from instantiate_contract event*/
    let contract_address = &event
        .attributes
        .iter()
        .find(|attr| attr.key == "contract_address")
        .ok_or_else(|| StdError::generic_err("cannot find `contract_address` attribute"))?
        .value;

    /* Update the state of the contract adding the new generated MINTED_TOKEN */
    MINTED_TOKENS.update(deps.storage, |mut tokens| -> StdResult<Vec<String>> {
        tokens.push(contract_address.to_string());

        Ok(tokens)
    })?;

    Ok(Response::new()
        .add_attribute("method", "handle_instantiate_reply")
        .add_attribute("contract_address", contract_address))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        /* Return the list of all tokens that were minted thru this contract */
        QueryMsg::GetMintedTokens {} => to_binary(&query_minted_tokens(deps)?),
    }
}

fn query_minted_tokens(deps: Deps) -> StdResult<MintedTokens> {
    Ok(MintedTokens {
        minted_tokens: MINTED_TOKENS.load(deps.storage)?,
    })
}

/* In case you want to upgrade this contract you can find information about
how to migrate the contract in the following link:
https://docs.terra.money/docs/develop/dapp/quick-start/contract-migration.html*/
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
    Ok(Response::default())
}

i. Close and save lib.rs.

j. Open test.rs and add the following:

#[cfg(test)]

mod tests {

    use crate::{
        contract::{execute, instantiate, query, reply},
        msg::{DepositType, ExecuteMsg, InstantiateMsg, MintedTokens, QueryMsg},
    };

    use cosmwasm_std::{
        from_binary,
        testing::{mock_dependencies, mock_env, mock_info},
        to_binary, Attribute, Coin, CosmosMsg, DepsMut, Event, Reply, Response, SubMsg,
        SubMsgExecutionResponse, Uint128, WasmMsg,
    };

    use cw20::{Cw20Coin, MinterResponse};

    #[test]
    fn test_instantiate() {
        // GIVEN
        let mut deps = mock_dependencies(&[]);

        // WHEN
        let res = do_instantiate(deps.as_mut());

        // THEN
        let attrs = res.attributes;

        assert_eq!(
            vec![
                Attribute {
                    key: "method".to_string(),

                    value: "instantiate".to_string()
                },
                Attribute {
                    key: "token_contract_code_id".to_string(),

                    value: "1".to_string()
                }
            ],
            attrs
        );
    }

    #[test]

    fn test_mint_token() {
        // GIVEN
        let mut deps = mock_dependencies(&[]);

        // WHEN
        do_instantiate(deps.as_mut());
        let res = do_mint_new_token(deps.as_mut());

        // THEN
        let res_attr = res.attributes;
        assert_eq!(1, res_attr.len());
        assert_eq!("instantiate_token", res_attr.get(0).unwrap().value);
        let res_message = res.messages;
        assert_eq!(1, res_message.len());
        let success_reply = SubMsg::reply_on_success(
            CosmosMsg::Wasm(WasmMsg::Instantiate {
                admin: Some("cosmos2contract".to_string()),

                code_id: 1,

                funds: vec![],

                msg: to_binary(&cw20_base::msg::InstantiateMsg {
                    name: "Bit Money".to_string(),

                    symbol: "BTM".to_string(),

                    decimals: 2,

                    mint: Some(MinterResponse {
                        minter: "cosmos2contract".to_string(),

                        cap: Some(Uint128::new(1234)),
                    }),

                    initial_balances: vec![Cw20Coin {
                        amount: Uint128::new(123),

                        address: "creator".to_string(),
                    }],

                    marketing: None,
                })
                .unwrap(),

                label: "Bit Money".to_string(),
            }),
            1,
        );

        assert_eq!(&success_reply, res_message.get(0).unwrap());
    }

    #[test]

    fn test_reply_instantiate_event() {
        // GIVEN
        let mut deps = mock_dependencies(&[]);
        let env = mock_env();
        let query_minted_tokens = QueryMsg::GetMintedTokens {};

        // WHEN
        do_instantiate(deps.as_mut());
        do_mint_new_token(deps.as_mut());
        let do_instantiate_res = do_reply_instantiate_event(deps.as_mut());
        let query_res = query(deps.as_ref(), env, query_minted_tokens).unwrap();
        let query_res: MintedTokens = from_binary(&query_res).unwrap();

        // THEN
        assert_eq!(
            Response::new()
                .add_attribute("method", "handle_instantiate_reply")
                .add_attribute("contract_address", "bit_money_contract_address"),
            do_instantiate_res
        );

        assert_eq!(
            MintedTokens {
                minted_tokens: vec!["bit_money_contract_address".to_string()]
            },
            query_res
        );
    }

    #[test]

    fn test_mint_existent_token() {
        // GIVEN
        let mut deps = mock_dependencies(&[]);
        let env = mock_env();
        let info = mock_info(
            "creator",
            &vec![Coin {
                denom: "uusd".to_string(),

                amount: Uint128::new(1),
            }],
        );
        let msg = ExecuteMsg::Deposit(DepositType::Mint {
            token_address: "bit_money_contract_address".to_string(),

            recipient: "creator".to_string(),
        });

        // WHEN
        do_instantiate(deps.as_mut());
        do_mint_new_token(deps.as_mut());
        do_reply_instantiate_event(deps.as_mut());
        let execute_res = execute(deps.as_mut(), env, info, msg).unwrap();

        // THEN
        assert_eq!(
            Response::new()
                .add_attribute("method", "mint")
                .add_messages(vec![CosmosMsg::Wasm(WasmMsg::Execute {
                    contract_addr: "bit_money_contract_address".to_string(),

                    msg: to_binary(&cw20_base::msg::ExecuteMsg::Mint {
                        amount: Uint128::new(1),

                        recipient: "creator".to_string()
                    })
                    .unwrap(),

                    funds: vec![],
                })]),
            execute_res
        );
    }

    #[test]

    fn test_burn_tokens() {
        // GIVEN
        let mut deps = mock_dependencies(&[]);
        let env = mock_env();
        let info = mock_info("creator", &[]);
        let exec_burn_tokens = ExecuteMsg::Burn {
            amount: Uint128::new(123),

            token_address: "bit_money_contract_address".to_string(),
        };

        // WHEN
        do_instantiate(deps.as_mut());
        do_reply_instantiate_event(deps.as_mut());
        do_mint_new_token(deps.as_mut());
        let res = execute(deps.as_mut(), env, info, exec_burn_tokens).unwrap();

        // THEN
        let res_attr = res.attributes;
        assert_eq!(1, res_attr.len());
        assert_eq!("burn", res_attr.get(0).unwrap().value);
        let res_message = res.messages;
        assert_eq!(2, res_message.len());
        let message_reply = SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute {
            contract_addr: "bit_money_contract_address".to_string(),

            msg: to_binary(&cw20_base::msg::ExecuteMsg::BurnFrom {
                owner: "creator".to_string(),

                amount: Uint128::new(123),
            })
            .unwrap(),

            funds: vec![],
        }));
        assert_eq!(vec![message_reply], res_message);
    }

    /*

    * HELPER METHODS TO DO NOT REPEAT CODE MANY TIMES

    */

    fn do_instantiate(deps: DepsMut) -> Response {
        let instantiate_msg = InstantiateMsg {
            stable_denom: "uusd".to_string(),
            token_contract_code_id: 1,
        };
        let info = mock_info("creator", &[]);
        let env = mock_env();
        instantiate(deps, env, info, instantiate_msg).unwrap()
    }

    fn do_mint_new_token(deps: DepsMut) -> Response {
        let env = mock_env();
        let info = mock_info(
            "i_am_the_sender",
            &vec![Coin {
                denom: "uusd".to_string(),

                amount: Uint128::new(123),
            }],
        );

        let token_msg = cw20_base::msg::InstantiateMsg {
            name: "Bit Money".to_string(),
            symbol: "BTM".to_string(),
            decimals: 2,
            mint: Some(MinterResponse {
                minter: "creator".to_string(),

                cap: Some(Uint128::new(1234)),
            }),
            initial_balances: vec![Cw20Coin {
                amount: Uint128::new(123),

                address: "creator".to_string(),
            }],
            marketing: None,
        };
        let msg = ExecuteMsg::Deposit(DepositType::Instantiate(token_msg.clone()));
        execute(deps, env.clone(), info.clone(), msg).unwrap()
    }

    /* Confirm reply event form instantiate method. That way
    the minted_tokens addresses can be whitelisted in factory.*/
    fn do_reply_instantiate_event(deps: DepsMut) -> Response {
        let env = mock_env();
        let event = Event::new("instantiate_contract")
            .add_attribute("creator", "token_factory_addr")
            .add_attribute("admin", "i_am_the_sender")
            .add_attribute("code_id", "1")
            .add_attribute("contract_address", "bit_money_contract_address");

        reply(
            deps,
            env,
            Reply {
                id: 1,

                result: cosmwasm_std::ContractResult::Ok(SubMsgExecutionResponse {
                    events: vec![event],

                    data: None,
                }),
            },
        )
        .unwrap()
    }
}

h. Navigate to token-factory/contracts/token-factory/examples.

i. Open schema.rs.

use std::env::current_dir;
use std::fs::create_dir_all;
use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
use token_factory::msg::{ExecuteMsg, QueryMsg};

fn main() {
    let mut out_dir = current_dir().unwrap();
    out_dir.push("schema");
    create_dir_all(&out_dir).unwrap();

    remove_schemas(&out_dir).unwrap();

    export_schema(&schema_for!(token_factory::msg::InstantiateMsg), &out_dir);
    export_schema(&schema_for!(ExecuteMsg), &out_dir);
    export_schema(&schema_for!(QueryMsg), &out_dir);
}

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 /tokens-factory/contracts/token-factory.

cd /tokens-factory/contracts/token-factory

b. Generate the schema:

 cargo schema

You should see output similar to:

Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/examples/schema`
Removing "~/Documents/github/tokens-factory/contracts/token-factory/schema/execute_msg.json" …
Removing "~/Documents/github/tokens-factory/contracts/token-factory/schema/instantiate_msg.json" …
Removing "~/Documents/github/tokens-factory/contracts/token-factory/schema/query_msg.json" …
Created ~/Documents/github/tokens-factory/contracts/token-factory/schema/instantiate_msg.json
Created ~/Documents/github/tokens-factory/contracts/token-factory/schema/execute_msg.json
Created ~/Documents/github/tokens-factory/contracts/token-factory/schema/query_msg.json

c. Run the tests:

cargo test

You will see output similar to the following:

Finished test [unoptimized + debuginfo] target(s) in 0.02s
Running unittests (target/debug/deps/token_factory-03f77bf897cd72b7)

running 5 tests
test test::tests::test_instantiate ... ok
test test::tests::test_burn_tokens ... ok
test test::tests::test_mint_token ... ok
test test::tests::test_mint_existent_token ... ok
test test::tests::test_reply_instantiate_event ... ok
test 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:

Tip

To determine which <token_contract_code_id>, check the file refs.terrain.json from the workspace’s root under the cw20-token-factory object.

{
  "_global": {
    "_base": {
      "instantiation": {
        "instantiateMsg": {
          "stable_denom": "uusd",
          "token_contract_code_id": <token_contract_id>
        }
      }
    }
  }, // ...
}

7. 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:

terrain deploy token-factory --signer test

Tip

If your code is not working as expected, you can clone the repo with all changes done until now.