var img = document.createElement('img'); img.src = "https://terradocs.matomo.cloud//piwik.php?idsite=1&rec=1&url=https://docs.terra.money" + location.pathname; img.style = "border:0"; img.alt = "tracker"; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(img,s);
Skip to main content

Build a CW20 token factory

In this tutorial, you'll build a CW20 token factory. A token factory allows any Cosmos SDK address (including a contract) to mint a 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

  1. Instantiate a new app using Terrain.
  1. When the app is generated, the following displays.
Terminal
CopyExpandClose
terrain new token_factory

Generate the cw20_factory_token contract

  1. Navigate to the token_factory directory.
  1. Create the cw20_factory_token contract.
  1. When the contract is generated, the following displays:
Terminal
CopyExpandClose
cd token_factory

Modify the mnemonic passphrase

Before editing the smart contracts you created in step 2, modify the mnemonic you'll use to deploy the contract to LocalTerra:

  1. Open /keys.terrain.js and set the mnemonic 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
Copy
💡tip

Because the wallet contains funds, it is recommended that you also import the passphrase listed below into the Station Extension. You can view other example mnemonics on Github:

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

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',
},
};
Copy

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 ensure that the development environment is configured correctly:

  1. Deploy the token_factory contract.
  1. Deploy the cw20_factory_token contract.
Terminal
CopyExpandClose
terrain deploy token_factory --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 for:

To add the CW20 Base to the cw20_factory_token contract, do the following.

  1. Navigate to the /cw20_factory_token/ directory.
  1. Open cargo.toml and add this to the dependencies:
Terminal
CopyExpandClose
cd /token_factory/contracts/cw20_factory_token/

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.

msg.rs

  1. Navigate to the /cw20_factory_token/src directory.
  1. Open msg.rs in your code editor and paste the following.
  1. Save and close msg.rs.
Terminal
CopyExpandClose

cd /token_factory/contracts/cw20_factory_token/src

lib.rs

  1. Open lib.rs and paste the following.
  1. Save and close lib.rs.
lib.rs
CopyExpandClose
pub mod contract;
pub mod msg;

contract.rs

  1. Open contract.rs and paste the following.
  1. Save and close contract.rs.
contract.rs
CopyExpandClose
#[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())
}

schema.rs

  1. Open schemas.rs and paste the following.
  1. Save and close schemas.rs.
schemas.rs
CopyExpandClose
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);
}

3. Generate and test the schema

  1. Navigate to /token_factory/contracts/cw20_factory_token.
  1. Generate the new schema.
  1. Test the schema.
Terminal
CopyExpandClose
cd /token_factory/contracts/cw20_factory_token

4. Modify config.terrain.json

  1. Open config.terrain.json.

  2. Modify the InstantiateMsg property in the config.terrain.json so that it contains the name, symbol, decimals, and initial_balances shown in the example. This allows you to send the correct data to the smart contract upon instantiation.

config.terrain.json
CopyExpandClose
{
"_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.

Terminal
CopyExpandClose
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 8da7892486704c54e33442b156d63178f5137527
Copy

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

  • cw2
  • cw20
  • cw20-base
  • cw20_factory_token

To add the dependencies, do the following.

  1. Navigate to /token_factory/contracts/token_factory.

  2. Open cargo.toml and add the dependencies inside the header.

cargo.toml
CopyExpandClose
# ...
[dependencies]
cw2 = "0.13.2"
cw20 = "0.13.2"
cw20-base = { version = "0.13.2", features = ["library"] }
cw20-factory-token = { version = "0.6.0", features = ["library"] }
# ...

2. Modify the contract files

Now that you've added the dependencies, you will need to 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:

  1. Navigate to /token_factory/contracts/token_factory/src.

  2. Open error.rs and add the following.

  1. Save and close error.rs.
  1. Open msg.rs and add the following.
  1. Save and close msg.rs.
  1. Open state.rs and add the following.
  1. Save and close state.rs.
  1. Open lib.rs and add the following.
  1. Save and close lib.rs.
  1. Open contract.rs and add the following.
  1. Open test.rs and add the following.
  1. Save and Close test.rs.
  1. Navigate to token_factory/contracts/token_factory/examples.
  1. Open schema.rs and paste the following.
  1. Save and close schema.rs.
error.rs
CopyExpandClose
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
},

}

3. Generate and test the schema

Now that you have modified the token_factory contract, generate the schema and run the tests to validate that the code works as expected:

  1. Navigate to /token_factory/contracts/token_factory.
  1. Generate the schema.
  1. You should see output similar to the following.
  1. Run the tests.
  1. You will see output similar to the following.
Terminal
CopyExpandClose
cd /token_factory/contracts/token_factory

4. Modify config.terrain.json

  1. Open config.terrain.json.

  2. 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": "uluna",
"token_contract_code_id": <token_contract_id>
}
}
}
}, // ...
}
Copy

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
Copy
💡tip

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.