dApp Docs/CW20代币标准与工厂实现
Development reference. Not independently verified for production.

MSG Chain CW20 代币标准与工厂实现 — 完整开发参考级指南

版本: v1.0.0
目标链: msg-chain-1
Bech32 前缀: msg
主网状态: No-Go
Gas 价格: 1,000,000,000 attoMSG/gas
MSG 精度: 18 位小数
签名方案: Dilithium-5 (后量子密码学)
共识: Round-Robin + DAR


目录

  1. CW20 标准详解
  2. 合约完整实现 — cw20_base
  3. CW20 工厂合约
  4. 代币部署
  5. 代币操作指南
  6. CW20 扩展实现
  7. 前端集成
  8. 安全考虑
  9. 完整示例项目
  10. 附录

1. CW20 标准详解

1.1 什么是 CW20?

CW20 是 CosmWasm 生态中的代币标准,类似于 Ethereum 上的 ERC20。它定义了同质化代币(Fungible Token)的通用接口,包括转账、授权、铸造、销毁等操作。MSG Chain 原生支持 CW20 标准,在 contracts/cosmwasm/all/ 中包含 cw20_base 作为标准合约包。

1.2 CW20 核心接口

CW20 标准由 cw20 crate 定义(https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md),包含以下核心组件:

1.2.1 ExecuteMsg — 执行消息

pub enum Cw20ExecuteMsg {
    /// 转账:将代币从 sender 发送到 recipient
    Transfer { recipient: String, amount: Uint128 },
    /// 授权转账:sender 授权 spender 可花费 amount 代币
    Approve { spender: String, amount: Uint128 },
    /// 授权转账(减少模式):将 spender 的额度减少 amount
    DecreaseAllowance { spender: String, amount: Uint128, expires: Option<Expiration> },
    /// 授权转账(增加模式):将 spender 的额度增加 amount
    IncreaseAllowance { spender: String, amount: Uint128, expires: Option<Expiration> },
    /// 转移授权代币:从 owner 处转移代币到 recipient
    TransferFrom { owner: String, recipient: String, amount: Uint128 },
    /// 发送代币到合约并触发接收处理
    Send { contract: String, amount: Uint128, msg: Binary },
    /// 销毁代币
    Burn { amount: Uint128 },
    /// 铸造代币(仅 minter 可调用)
    Mint { recipient: String, amount: Uint128 },
    /// 更新 minter(仅当前 minter 可调用)
    UpdateMinter { new_minter: Option<String> },
    /// 设置营销信息
    SetMarketing { project: Option<String>, description: Option<String>, marketing: Option<String> },
    /// 上传 logo
    UploadLogo(Logo),
    /// 使用授权发送到合约
    SendFrom { owner: String, contract: String, amount: Uint128, msg: Binary },
    /// 使用授权销毁
    BurnFrom { owner: String, amount: Uint128 },
}

1.2.2 QueryMsg — 查询消息

pub enum Cw20QueryMsg {
    /// 查询代币余额
    Balance { address: String },
    /// 查询代币信息(名称、符号、精度、总量)
    TokenInfo {},
    /// 查询铸造权限
    Minter {},
    /// 查询授权额度
    Allowance { owner: String, spender: String },
    /// 查询所有持有者账户(需 Enumerable 扩展)
    AllAccounts { start_after: Option<String>, limit: Option<u32> },
    /// 查询所有授权(需 Enumerable 扩展)
    AllAllowances { owner: String, start_after: Option<String>, limit: Option<u32> },
    /// 查询营销信息
    MarketingInfo {},
    /// 查询下载 logo 信息
    DownloadLogo {},
}

1.2.3 响应类型

pub struct BalanceResponse { pub balance: Uint128 }
pub struct TokenInfoResponse { pub name: String, pub symbol: String, pub decimals: u8, pub total_supply: Uint128 }
pub struct MinterResponse { pub minter: Option<String>, pub cap: Option<Uint128> }
pub struct AllowanceResponse { pub allowance: Uint128, pub expires: Expiration }
pub struct AllAccountsResponse { pub accounts: Vec<String> }
pub struct AllAllowancesResponse { pub allowances: Vec<AllowanceInfo> }
pub struct AllowanceInfo { pub owner: String, pub spender: String, pub allowance: Uint128, pub expires: Expiration }
pub struct MarketingInfoResponse { pub project: Option<String>, pub description: Option<String>, pub marketing: Option<String>, pub logo: Option<LogoInfo> }
pub enum LogoInfo { Url(String), Embedded }

1.3 数据存储结构

CW20 标准合约使用以下存储布局:

pub const TOKEN_INFO: Item<TokenInfo> = Item::new("token_info");
pub const BALANCES: Map<&Addr, Uint128> = Map::new("balance");
pub const ALLOWANCES: Map<(&Addr, &Addr), Allowance> = Map::new("allowance");

pub struct TokenInfo {
    pub name: String,
    pub symbol: String,
    pub decimals: u8,
    pub total_supply: Uint128,
    pub mint: Option<MinterData>,
}

pub struct MinterData {
    pub minter: Addr,
    pub cap: Option<Uint128>,
}

pub struct Allowance {
    pub allowance: Uint128,
    pub expires: Expiration,
}

1.4 CW20 扩展功能

CW20 标准通过特性标记支持以下扩展:

扩展 描述 必需字段
Allowances 授权转账(Approve/TransferFrom) 基本功能
Mintable 可铸造(Mint/UpdateMinter) mint 字段
Burnable 可销毁(Burn) 基本功能
Marketing 营销信息(SetMarketing/UploadLogo) 可选
Enumerable 可枚举(AllAccounts/AllAllowances) 可选

1.5 与 ERC20 的关键差异

特性 ERC20 CW20
授权方式 approve 设置绝对额度 approve 设置绝对额度
授权保护 无默认保护 IncreaseAllowance / DecreaseAllowance 可防重入
过期时间 无 Expiration 支持高度/时间过期
元信息 symbol/name/decimals 固定 链上可更新的营销信息
总量追踪 totalSupply() TokenInfo.total_supply
合约接收 需要额外注册 Send 消息 + Cw20ReceiveMsg 回调

1.6 Expiration 类型

pub enum Expiration {
    AtHeight(u64),          // 在指定区块高度后过期
    AtTime(Timestamp),      // 在指定时间戳(秒)后过期
    Never {},               // 永不过期
}

impl Expiration {
    pub fn is_expired(&self, height: u64, time: u64) -> bool {
        match self {
            Expiration::AtHeight(h) => height >= *h,
            Expiration::AtTime(t) => time >= t.seconds(),
            Expiration::Never {} => false,
        }
    }
}

1.7 Cw20ReceiveMsg — 合约接收回调

当使用 Send 消息转账到合约时,目标合约会收到:

pub struct Cw20ReceiveMsg {
    pub sender: String,     // 发送者地址
    pub amount: Uint128,    // 发送数量
    pub msg: Binary,        // 可选的回调数据
}

目标合约需要实现 receive 处理函数来解析此消息。这是 CW20 与 ERC20 的关键区别之一——CW20 原生支持代币+数据的一步式操作(类似 ERC20 的 approve + transferFrom 合一但更安全)。


2. 合约完整实现 — cw20_base

2.1 项目结构

cw20-base/
├── Cargo.toml
├── src/
│   ├── lib.rs              # 库入口
│   ├── contract.rs         # 合约入口点
│   ├── msg.rs              # 消息类型
│   ├── state.rs            # 状态存储
│   ├── enumerable.rs       # Enumerable 扩展
│   ├── allowances.rs       # Allowances 扩展
│   └── marketing.rs        # Marketing 扩展
├── examples/
│   └── schema.rs
├── tests/
│   └── integration.rs
└── schema/

2.2 Cargo.toml

[package]
name = "cw20-base"
version = "1.1.0"
edition = "2021"
description = "MSG Chain CW20 base token contract"

[lib]
crate-type = ["cdylib", "rlib"]

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = "abort"
incremental = false
overflow-checks = true

[dependencies]
cosmwasm-std = "1.5"
cosmwasm-storage = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
cw20 = "1.1"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
uint = "0.9"

[dev-dependencies]
cosmwasm-vm = "1.5"
cw-multi-test = "0.18"
anyhow = "1.0"

2.3 src/state.rs

use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Binary, StdResult, Storage, Uint128};
use cw_storage_plus::{Item, Map};

#[cw_serde]
pub struct TokenInfo {
    pub name: String,
    pub symbol: String,
    pub decimals: u8,
    pub total_supply: Uint128,
    pub mint: Option<MinterData>,
}

#[cw_serde]
pub struct MinterData {
    pub minter: Addr,
    pub cap: Option<Uint128>,
}

#[cw_serde]
pub struct Allowance {
    pub allowance: Uint128,
    pub expires: Expiration,
}

#[cw_serde]
pub enum Expiration {
    AtHeight(u64),
    AtTime(u64),
    Never {},
}

impl Expiration {
    pub fn is_expired(&self, height: u64, time: u64) -> bool {
        match self {
            Expiration::AtHeight(h) => height >= *h,
            Expiration::AtTime(t) => time >= *t,
            Expiration::Never {} => false,
        }
    }
}

pub const TOKEN_INFO: Item<TokenInfo> = Item::new("token_info");
pub const BALANCES: Map<&Addr, Uint128> = Map::new("balance");
pub const ALLOWANCES: Map<(&Addr, &Addr), Allowance> = Map::new("allowance");

pub fn validate_address(api: &dyn cosmwasm_std::Api, addr: &str) -> StdResult<Addr> {
    api.addr_validate(addr)
}

2.4 src/msg.rs

use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Binary, Uint128};
use crate::state::Expiration;

#[cw_serde]
pub struct InstantiateMsg {
    pub name: String,
    pub symbol: String,
    pub decimals: u8,
    pub initial_balances: Vec<Cw20Coin>,
    pub mint: Option<MinterResponse>,
    pub marketing: Option<InstantiateMarketingInfo>,
}

#[cw_serde]
pub struct Cw20Coin {
    pub address: String,
    pub amount: Uint128,
}

#[cw_serde]
pub struct MinterResponse {
    pub minter: String,
    pub cap: Option<Uint128>,
}

#[cw_serde]
pub struct InstantiateMarketingInfo {
    pub project: Option<String>,
    pub description: Option<String>,
    pub marketing: Option<String>,
    pub logo: Option<Logo>,
}

#[cw_serde]
pub enum Logo {
    Url(String),
    Embedded(Binary),
}

#[cw_serde]
pub enum ExecuteMsg {
    Transfer { recipient: String, amount: Uint128 },
    Burn { amount: Uint128 },
    Send { contract: String, amount: Uint128, msg: Binary },
    Mint { recipient: String, amount: Uint128 },
    UpdateMinter { new_minter: Option<String> },
    Approve { spender: String, amount: Uint128, expires: Option<Expiration> },
    IncreaseAllowance { spender: String, amount: Uint128, expires: Option<Expiration> },
    DecreaseAllowance { spender: String, amount: Uint128, expires: Option<Expiration> },
    TransferFrom { owner: String, recipient: String, amount: Uint128 },
    BurnFrom { owner: String, amount: Uint128 },
    SendFrom { owner: String, contract: String, amount: Uint128, msg: Binary },
    SetMarketing { project: Option<String>, description: Option<String>, marketing: Option<String> },
    UploadLogo(Logo),
}

#[cw_serde]
pub enum QueryMsg {
    Balance { address: String },
    TokenInfo {},
    Minter {},
    Allowance { owner: String, spender: String },
    AllAccounts { start_after: Option<String>, limit: Option<u32> },
    AllAllowances { owner: String, start_after: Option<String>, limit: Option<u32> },
    MarketingInfo {},
    DownloadLogo {},
}

#[cw_serde]
pub struct BalanceResponse { pub balance: Uint128 }
#[cw_serde]
pub struct TokenInfoResponse { pub name: String, pub symbol: String, pub decimals: u8, pub total_supply: Uint128 }
#[cw_serde]
pub struct AllowanceResponse { pub allowance: Uint128, pub expires: Expiration }
#[cw_serde]
pub struct AllAccountsResponse { pub accounts: Vec<String> }
#[cw_serde]
pub struct AllAllowancesResponse { pub allowances: Vec<AllowanceInfo> }
#[cw_serde]
pub struct AllowanceInfo { pub owner: String, pub spender: String, pub allowance: Uint128, pub expires: Expiration }
#[cw_serde]
pub struct MarketingInfoResponse { pub project: Option<String>, pub description: Option<String>, pub marketing: Option<String>, pub logo: Option<LogoInfo> }
#[cw_serde]
pub enum LogoInfo { Url(String), Embedded }

2.5 src/error.rs

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

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

    #[error("Unauthorized - sender is not the minter")]
    Unauthorized {},

    #[error("Cannot transfer zero amount")]
    ZeroAmount {},

    #[error("Insufficient balance: balance={balance}, required={required}")]
    InsufficientBalance { balance: Uint128, required: Uint128 },

    #[error("Insufficient allowance: allowance={allowance}, required={required}")]
    InsufficientAllowance { allowance: Uint128, required: Uint128 },

    #[error("Allowance is expired")]
    Expired {},

    #[error("Mint cap exceeded: cap={cap}, total_supply would be {total}")]
    CapExceeded { cap: Uint128, total: Uint128 },

    #[error("Minting is not allowed on this token")]
    NotMintable {},

    #[error("Logo binary too large: {size} bytes, max {max} bytes")]
    LogoTooLarge { size: u64, max: u64 },
}

2.6 src/contract.rs — 完整实现

use cosmwasm_std::{
    entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo,
    Response, StdResult, Uint128, WasmMsg, CosmosMsg, StdError,
};
use cw2::set_contract_version;

use crate::error::ContractError;
use crate::msg::{
    AllAccountsResponse, AllAllowancesResponse, AllowanceInfo,
    AllowanceResponse, BalanceResponse, ExecuteMsg, InstantiateMsg,
    MarketingInfoResponse, MinterResponse, QueryMsg, TokenInfoResponse,
};
use crate::state::{
    validate_address, Allowance, Expiration, MinterData, TokenInfo,
    ALLOWANCES, BALANCES, TOKEN_INFO,
};

const CONTRACT_NAME: &str = "cw20-base";
const CONTRACT_VERSION: &str = "1.1.0";

#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    if msg.decimals > 18 {
        return Err(StdError::generic_err("Decimals must not exceed 18"));
    }
    let mint = match msg.mint {
        Some(m) => {
            let minter_addr = deps.api.addr_validate(&m.minter)?;
            Some(MinterData { minter: minter_addr, cap: m.cap })
        }
        None => None,
    };
    let total_supply = msg.initial_balances.iter()
        .map(|c| c.amount).sum::<Uint128>();
    let info = TokenInfo {
        name: msg.name, symbol: msg.symbol, decimals: msg.decimals,
        total_supply, mint,
    };
    TOKEN_INFO.save(deps.storage, &info)?;
    for coin in &msg.initial_balances {
        let addr = deps.api.addr_validate(&coin.address)?;
        BALANCES.save(deps.storage, &addr, &coin.amount)?;
    }
    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("name", &info.name)
        .add_attribute("symbol", &info.symbol)
        .add_attribute("decimals", info.decimals.to_string())
        .add_attribute("total_supply", info.total_supply.to_string()))
}

#[entry_point]
pub fn execute(
    deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Transfer { recipient, amount } => execute_transfer(deps, env, info, recipient, amount),
        ExecuteMsg::Burn { amount } => execute_burn(deps, env, info, amount),
        ExecuteMsg::Mint { recipient, amount } => execute_mint(deps, env, info, recipient, amount),
        ExecuteMsg::UpdateMinter { new_minter } => execute_update_minter(deps, env, info, new_minter),
        ExecuteMsg::Approve { spender, amount, expires } => execute_approve(deps, env, info, spender, amount, expires),
        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::Send { contract, amount, msg } => execute_send(deps, env, info, contract, amount, msg),
        ExecuteMsg::SendFrom { owner, contract, amount, msg } => execute_send_from(deps, env, info, owner, contract, amount, msg),
        ExecuteMsg::SetMarketing { project, description, marketing } => execute_set_marketing(deps, env, info, project, description, marketing),
        ExecuteMsg::UploadLogo(logo) => execute_upload_logo(deps, env, info, logo),
    }
}

fn check_balance(storage: &dyn cosmwasm_std::Storage, owner: &Addr, amount: Uint128) -> Result<(), ContractError> {
    let balance = BALANCES.may_load(storage, owner)?.unwrap_or_default();
    if balance < amount {
        return Err(ContractError::InsufficientBalance { balance, required: amount });
    }
    Ok(())
}

fn transfer_balance(storage: &mut dyn cosmwasm_std::Storage, from: &Addr, to: &Addr, amount: Uint128) -> Result<(), ContractError> {
    if amount.is_zero() { return Err(ContractError::ZeroAmount {}); }
    BALANCES.update(storage, from, |bal: Option<Uint128>| -> StdResult<_> {
        Ok(bal.unwrap_or_default().checked_sub(amount).map_err(|_| StdError::generic_err("Insufficient balance"))?)
    })?;
    BALANCES.update(storage, to, |bal: Option<Uint128>| -> StdResult<_> {
        Ok(bal.unwrap_or_default() + amount)
    })?;
    Ok(())
}

fn execute_transfer(deps: DepsMut, _env: Env, info: MessageInfo, recipient: String, amount: Uint128) -> Result<Response, ContractError> {
    if amount.is_zero() { return Err(ContractError::ZeroAmount {}); }
    let rcpt = deps.api.addr_validate(&recipient)?;
    transfer_balance(deps.storage, &info.sender, &rcpt, amount)?;
    Ok(Response::new()
        .add_attribute("action", "transfer")
        .add_attribute("from", info.sender.as_str())
        .add_attribute("to", &recipient)
        .add_attribute("amount", amount.to_string()))
}

fn execute_burn(deps: DepsMut, _env: Env, info: MessageInfo, amount: Uint128) -> Result<Response, ContractError> {
    if amount.is_zero() { return Err(ContractError::ZeroAmount {}); }
    let sender = &info.sender;
    check_balance(deps.storage, sender, amount)?;
    BALANCES.update(deps.storage, sender, |bal: Option<Uint128>| -> StdResult<_> {
        Ok(bal.unwrap_or_default().checked_sub(amount)?)
    })?;
    TOKEN_INFO.update(deps.storage, |mut info| -> StdResult<_> {
        info.total_supply = info.total_supply.checked_sub(amount)?;
        Ok(info)
    })?;
    Ok(Response::new()
        .add_attribute("action", "burn")
        .add_attribute("from", sender.as_str())
        .add_attribute("amount", amount.to_string()))
}

fn execute_mint(deps: DepsMut, _env: Env, info: MessageInfo, recipient: String, amount: Uint128) -> Result<Response, ContractError> {
    if amount.is_zero() { return Err(ContractError::ZeroAmount {}); }
    let token_info = TOKEN_INFO.load(deps.storage)?;
    let mint = token_info.mint.as_ref().ok_or(ContractError::NotMintable {})?;
    if info.sender != mint.minter { return Err(ContractError::Unauthorized {}); }
    let new_supply = token_info.total_supply.checked_add(amount)
        .map_err(|_| StdError::generic_err("Total supply overflow"))?;
    if let Some(cap) = mint.cap {
        if new_supply > cap { return Err(ContractError::CapExceeded { cap, total: new_supply }); }
    }
    let rcpt = deps.api.addr_validate(&recipient)?;
    BALANCES.update(deps.storage, &rcpt, |bal: Option<Uint128>| -> StdResult<_> {
        Ok(bal.unwrap_or_default() + amount)
    })?;
    TOKEN_INFO.update(deps.storage, |mut info| -> StdResult<_> {
        info.total_supply = new_supply;
        Ok(info)
    })?;
    Ok(Response::new()
        .add_attribute("action", "mint")
        .add_attribute("to", &recipient)
        .add_attribute("amount", amount.to_string()))
}

fn execute_update_minter(deps: DepsMut, _env: Env, info: MessageInfo, new_minter: Option<String>) -> Result<Response, ContractError> {
    let token_info = TOKEN_INFO.load(deps.storage)?;
    let mint = token_info.mint.as_ref().ok_or(ContractError::NotMintable {})?;
    if info.sender != mint.minter { return Err(ContractError::Unauthorized {}); }
    let new_minter_addr = match &new_minter {
        Some(addr) => Some(deps.api.addr_validate(addr)?),
        None => None,
    };
    TOKEN_INFO.update(deps.storage, |mut info| -> StdResult<_> {
        info.mint = Some(MinterData {
            minter: new_minter_addr.clone().unwrap_or(mint.minter.clone()),
            cap: mint.cap,
        });
        Ok(info)
    })?;
    Ok(Response::new()
        .add_attribute("action", "update_minter")
        .add_attribute("new_minter", new_minter.unwrap_or_default()))
}

fn check_allowance(storage: &dyn cosmwasm_std::Storage, owner: &Addr, spender: &Addr, amount: Uint128, height: u64, time: u64) -> Result<(), ContractError> {
    let allowance = ALLOWANCES.may_load(storage, (owner, spender))?
        .unwrap_or(Allowance { allowance: Uint128::zero(), expires: Expiration::Never {} });
    if allowance.expires.is_expired(height, time) { return Err(ContractError::Expired {}); }
    if allowance.allowance < amount { return Err(ContractError::InsufficientAllowance { allowance: allowance.allowance, required: amount }); }
    Ok(())
}

fn execute_approve(deps: DepsMut, _env: Env, info: MessageInfo, spender: String, amount: Uint128, expires: Option<Expiration>) -> Result<Response, ContractError> {
    let spender_addr = deps.api.addr_validate(&spender)?;
    let expires = expires.unwrap_or(Expiration::Never {});
    ALLOWANCES.save(deps.storage, (&info.sender, &spender_addr), &Allowance { allowance: amount, expires })?;
    Ok(Response::new()
        .add_attribute("action", "approve")
        .add_attribute("owner", info.sender.as_str())
        .add_attribute("spender", &spender)
        .add_attribute("amount", amount.to_string()))
}

fn execute_increase_allowance(deps: DepsMut, _env: Env, info: MessageInfo, spender: String, amount: Uint128, expires: Option<Expiration>) -> Result<Response, ContractError> {
    let spender_addr = deps.api.addr_validate(&spender)?;
    let allowance = ALLOWANCES.may_load(deps.storage, (&info.sender, &spender_addr))?
        .unwrap_or(Allowance { allowance: Uint128::zero(), expires: Expiration::Never {} });
    let new_expires = expires.unwrap_or(allowance.expires);
    ALLOWANCES.save(deps.storage, (&info.sender, &spender_addr), &Allowance {
        allowance: allowance.allowance.checked_add(amount)?,
        expires: new_expires,
    })?;
    Ok(Response::new()
        .add_attribute("action", "increase_allowance")
        .add_attribute("owner", info.sender.as_str())
        .add_attribute("spender", &spender)
        .add_attribute("amount", amount.to_string()))
}

fn execute_decrease_allowance(deps: DepsMut, _env: Env, info: MessageInfo, spender: String, amount: Uint128, expires: Option<Expiration>) -> Result<Response, ContractError> {
    let spender_addr = deps.api.addr_validate(&spender)?;
    let allowance = ALLOWANCES.may_load(deps.storage, (&info.sender, &spender_addr))?
        .unwrap_or(Allowance { allowance: Uint128::zero(), expires: Expiration::Never {} });
    let new_expires = expires.unwrap_or(allowance.expires);
    ALLOWANCES.save(deps.storage, (&info.sender, &spender_addr), &Allowance {
        allowance: allowance.allowance.checked_sub(amount)?,
        expires: new_expires,
    })?;
    Ok(Response::new()
        .add_attribute("action", "decrease_allowance")
        .add_attribute("owner", info.sender.as_str())
        .add_attribute("spender", &spender)
        .add_attribute("amount", amount.to_string()))
}

fn execute_transfer_from(deps: DepsMut, env: Env, info: MessageInfo, owner: String, recipient: String, amount: Uint128) -> Result<Response, ContractError> {
    let owner_addr = deps.api.addr_validate(&owner)?;
    let rcpt = deps.api.addr_validate(&recipient)?;
    let spender = &info.sender;
    check_allowance(deps.storage, &owner_addr, spender, amount, env.block.height, env.block.time.seconds())?;
    ALLOWANCES.update(deps.storage, (&owner_addr, spender), |allow| -> StdResult<_> {
        let mut a = allow.unwrap_or(Allowance { allowance: Uint128::zero(), expires: Expiration::Never {} });
        a.allowance = a.allowance.checked_sub(amount)?;
        Ok(a)
    })?;
    transfer_balance(deps.storage, &owner_addr, &rcpt, amount)?;
    Ok(Response::new()
        .add_attribute("action", "transfer_from")
        .add_attribute("from", &owner)
        .add_attribute("to", &recipient)
        .add_attribute("by", spender.as_str())
        .add_attribute("amount", amount.to_string()))
}

fn execute_burn_from(deps: DepsMut, env: Env, info: MessageInfo, owner: String, amount: Uint128) -> Result<Response, ContractError> {
    let owner_addr = deps.api.addr_validate(&owner)?;
    let spender = &info.sender;
    check_allowance(deps.storage, &owner_addr, spender, amount, env.block.height, env.block.time.seconds())?;
    ALLOWANCES.update(deps.storage, (&owner_addr, spender), |allow| -> StdResult<_> {
        let mut a = allow.unwrap_or(Allowance { allowance: Uint128::zero(), expires: Expiration::Never {} });
        a.allowance = a.allowance.checked_sub(amount)?;
        Ok(a)
    })?;
    BALANCES.update(deps.storage, &owner_addr, |bal: Option<Uint128>| -> StdResult<_> {
        Ok(bal.unwrap_or_default().checked_sub(amount)?)
    })?;
    TOKEN_INFO.update(deps.storage, |mut info| -> StdResult<_> {
        info.total_supply = info.total_supply.checked_sub(amount)?;
        Ok(info)
    })?;
    Ok(Response::new()
        .add_attribute("action", "burn_from")
        .add_attribute("from", &owner)
        .add_attribute("by", spender.as_str())
        .add_attribute("amount", amount.to_string()))
}

fn execute_send(deps: DepsMut, _env: Env, info: MessageInfo, contract: String, amount: Uint128, msg: Binary) -> Result<Response, ContractError> {
    let contract_addr = deps.api.addr_validate(&contract)?;
    transfer_balance(deps.storage, &info.sender, &contract_addr, amount)?;
    let send_msg = CosmosMsg::Wasm(WasmMsg::Execute {
        contract_addr: contract,
        msg: to_binary(&cw20::Cw20ReceiveMsg {
            sender: info.sender.to_string(),
            amount,
            msg,
        })?,
        funds: vec![],
    });
    Ok(Response::new()
        .add_message(send_msg)
        .add_attribute("action", "send")
        .add_attribute("from", info.sender.as_str())
        .add_attribute("to", contract_addr.as_str())
        .add_attribute("amount", amount.to_string()))
}

fn execute_send_from(deps: DepsMut, env: Env, info: MessageInfo, owner: String, contract: String, amount: Uint128, msg: Binary) -> Result<Response, ContractError> {
    let owner_addr = deps.api.addr_validate(&owner)?;
    let contract_addr = deps.api.addr_validate(&contract)?;
    let spender = &info.sender;
    check_allowance(deps.storage, &owner_addr, spender, amount, env.block.height, env.block.time.seconds())?;
    ALLOWANCES.update(deps.storage, (&owner_addr, spender), |allow| -> StdResult<_> {
        let mut a = allow.unwrap_or(Allowance { allowance: Uint128::zero(), expires: Expiration::Never {} });
        a.allowance = a.allowance.checked_sub(amount)?;
        Ok(a)
    })?;
    transfer_balance(deps.storage, &owner_addr, &contract_addr, amount)?;
    let send_msg = CosmosMsg::Wasm(WasmMsg::Execute {
        contract_addr: contract,
        msg: to_binary(&cw20::Cw20ReceiveMsg { sender: owner, amount, msg })?,
        funds: vec![],
    });
    Ok(Response::new()
        .add_message(send_msg)
        .add_attribute("action", "send_from")
        .add_attribute("from", owner_addr.as_str())
        .add_attribute("to", contract_addr.as_str())
        .add_attribute("by", spender.as_str())
        .add_attribute("amount", amount.to_string()))
}

fn execute_set_marketing(deps: DepsMut, _env: Env, info: MessageInfo, project: Option<String>, description: Option<String>, marketing: Option<String>) -> Result<Response, ContractError> {
    crate::marketing::set_marketing(deps, info, project, description, marketing)
}

fn execute_upload_logo(deps: DepsMut, _env: Env, info: MessageInfo, logo: crate::msg::Logo) -> Result<Response, ContractError> {
    crate::marketing::upload_logo(deps, info, logo)
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        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::AllAccounts { start_after, limit } => to_binary(&query_all_accounts(deps, start_after, limit)?),
        QueryMsg::AllAllowances { owner, start_after, limit } => to_binary(&query_all_allowances(deps, owner, start_after, limit)?),
        QueryMsg::MarketingInfo {} => to_binary(&crate::marketing::query_marketing_info(deps)?),
        QueryMsg::DownloadLogo {} => to_binary(&crate::marketing::query_download_logo(deps)?),
    }
}

fn query_balance(deps: Deps, address: String) -> StdResult<BalanceResponse> {
    let addr = deps.api.addr_validate(&address)?;
    let balance = BALANCES.may_load(deps.storage, &addr)?.unwrap_or_default();
    Ok(BalanceResponse { balance })
}

fn query_token_info(deps: Deps) -> StdResult<TokenInfoResponse> {
    let info = TOKEN_INFO.load(deps.storage)?;
    Ok(TokenInfoResponse { name: info.name, symbol: info.symbol, decimals: info.decimals, total_supply: info.total_supply })
}

fn query_minter(deps: Deps) -> StdResult<MinterResponse> {
    let info = TOKEN_INFO.load(deps.storage)?;
    match info.mint {
        Some(m) => Ok(MinterResponse { minter: Some(m.minter.to_string()), cap: m.cap }),
        None => Ok(MinterResponse { minter: None, cap: None }),
    }
}

fn query_allowance(deps: Deps, owner: String, spender: String) -> StdResult<AllowanceResponse> {
    let owner_addr = deps.api.addr_validate(&owner)?;
    let spender_addr = deps.api.addr_validate(&spender)?;
    let allowance = ALLOWANCES.may_load(deps.storage, (&owner_addr, &spender_addr))?
        .unwrap_or(Allowance { allowance: Uint128::zero(), expires: Expiration::Never {} });
    Ok(AllowanceResponse { allowance: allowance.allowance, expires: allowance.expires })
}

fn query_all_accounts(deps: Deps, start_after: Option<String>, limit: Option<u32>) -> StdResult<AllAccountsResponse> {
    let limit = limit.unwrap_or(20).min(100) as usize;
    let start = start_after.as_deref().map(|s| deps.api.addr_validate(s)).transpose()?;
    let accounts: StdResult<Vec<_>> = BALANCES
        .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending)
        .filter_map(|r| r.ok())
        .skip(start.map(|s| s.as_str().len()).unwrap_or(0))
        .take(limit)
        .map(|a| Ok(a.to_string()))
        .collect();
    Ok(AllAccountsResponse { accounts: accounts? })
}

fn query_all_allowances(deps: Deps, owner: String, start_after: Option<String>, limit: Option<u32>) -> StdResult<AllAllowancesResponse> {
    let limit = limit.unwrap_or(20).min(100) as usize;
    let owner_addr = deps.api.addr_validate(&owner)?;
    let allowances: StdResult<Vec<_>> = ALLOWANCES
        .prefix(&owner_addr)
        .range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
        .take(limit)
        .map(|r| {
            let (spender, allowance) = r?;
            Ok(AllowanceInfo { owner: owner.clone(), spender: spender.to_string(), allowance: allowance.allowance, expires: allowance.expires })
        })
        .collect();
    Ok(AllAllowancesResponse { allowances: allowances? })
}

2.7 单元测试

#[cfg(test)]
mod tests {
    use super::*;
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::{from_binary, Addr};

    fn setup_test_token(deps: DepsMut) {
        let msg = InstantiateMsg {
            name: "TestToken".to_string(),
            symbol: "TST".to_string(),
            decimals: 18,
            initial_balances: vec![
                Cw20Coin { address: "msg1alice".to_string(), amount: Uint128::new(1000) },
                Cw20Coin { address: "msg1bob".to_string(), amount: Uint128::new(1000) },
            ],
            mint: Some(MinterResponse { minter: "msg1minter".to_string(), cap: Some(Uint128::new(10000)) }),
            marketing: None,
        };
        instantiate(deps, mock_env(), mock_info("creator", &[]), msg).unwrap();
    }

    #[test]
    fn test_instantiate_basic() {
        let mut deps = mock_dependencies();
        let msg = InstantiateMsg {
            name: "MyToken".to_string(), symbol: "MTK".to_string(), decimals: 18,
            initial_balances: vec![], mint: None, marketing: None,
        };
        instantiate(deps.as_mut(), mock_env(), mock_info("creator", &[]), msg).unwrap();
        let token_info = TOKEN_INFO.load(&deps.storage).unwrap();
        assert_eq!(token_info.name, "MyToken");
        assert_eq!(token_info.symbol, "MTK");
        assert_eq!(token_info.decimals, 18);
        assert_eq!(token_info.total_supply, Uint128::zero());
    }

    #[test]
    fn test_instantiate_with_initial_balances() {
        let mut deps = mock_dependencies();
        let msg = InstantiateMsg {
            name: "Token".to_string(), symbol: "TKN".to_string(), decimals: 6,
            initial_balances: vec![
                Cw20Coin { address: "msg1creator".to_string(), amount: Uint128::new(1000) },
                Cw20Coin { address: "msg1user".to_string(), amount: Uint128::new(500) },
            ],
            mint: Some(MinterResponse { minter: "msg1minter".to_string(), cap: Some(Uint128::new(10000)) }),
            marketing: None,
        };
        instantiate(deps.as_mut(), mock_env(), mock_info("creator", &[]), msg).unwrap();
        let token_info = TOKEN_INFO.load(&deps.storage).unwrap();
        assert_eq!(token_info.total_supply, Uint128::new(1500));
        let bal = BALANCES.load(&deps.storage, &Addr::unchecked("msg1user")).unwrap();
        assert_eq!(bal, Uint128::new(500));
    }

    #[test]
    fn test_transfer() {
        let mut deps = mock_dependencies();
        setup_test_token(deps.as_mut());
        let msg = ExecuteMsg::Transfer { recipient: "msg1bob".to_string(), amount: Uint128::new(300) };
        execute(deps.as_mut(), mock_env(), mock_info("msg1alice", &[]), msg).unwrap();
        let alice_bal = BALANCES.load(&deps.storage, &Addr::unchecked("msg1alice")).unwrap();
        assert_eq!(alice_bal, Uint128::new(700));
        let bob_bal = BALANCES.load(&deps.storage, &Addr::unchecked("msg1bob")).unwrap();
        assert_eq!(bob_bal, Uint128::new(1300));
    }

    #[test]
    fn test_transfer_insufficient_balance() {
        let mut deps = mock_dependencies();
        setup_test_token(deps.as_mut());
        let msg = ExecuteMsg::Transfer { recipient: "msg1bob".to_string(), amount: Uint128::new(2000) };
        let err = execute(deps.as_mut(), mock_env(), mock_info("msg1alice", &[]), msg).unwrap_err();
        assert!(matches!(err, ContractError::InsufficientBalance { .. }));
    }

    #[test]
    fn test_approve_and_transfer_from() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        setup_test_token(deps.as_mut());
        let info = mock_info("msg1alice", &[]);
        execute(deps.as_mut(), env.clone(), info, ExecuteMsg::Approve {
            spender: "msg1bob".to_string(), amount: Uint128::new(100), expires: None,
        }).unwrap();
        let allowance = ALLOWANCES.load(&deps.storage, (&Addr::unchecked("msg1alice"), &Addr::unchecked("msg1bob"))).unwrap();
        assert_eq!(allowance.allowance, Uint128::new(100));
        let info = mock_info("msg1bob", &[]);
        execute(deps.as_mut(), env.clone(), info, ExecuteMsg::TransferFrom {
            owner: "msg1alice".to_string(), recipient: "msg1charlie".to_string(), amount: Uint128::new(50),
        }).unwrap();
        let alice_bal = BALANCES.load(&deps.storage, &Addr::unchecked("msg1alice")).unwrap();
        assert_eq!(alice_bal, Uint128::new(950));
        let remaining = ALLOWANCES.load(&deps.storage, (&Addr::unchecked("msg1alice"), &Addr::unchecked("msg1bob"))).unwrap();
        assert_eq!(remaining.allowance, Uint128::new(50));
    }

    #[test]
    fn test_burn() {
        let mut deps = mock_dependencies();
        setup_test_token(deps.as_mut());
        execute(deps.as_mut(), mock_env(), mock_info("msg1alice", &[]), ExecuteMsg::Burn { amount: Uint128::new(200) }).unwrap();
        let alice_bal = BALANCES.load(&deps.storage, &Addr::unchecked("msg1alice")).unwrap();
        assert_eq!(alice_bal, Uint128::new(800));
        let token_info = TOKEN_INFO.load(&deps.storage).unwrap();
        assert_eq!(token_info.total_supply, Uint128::new(1800));
    }

    #[test]
    fn test_mint() {
        let mut deps = mock_dependencies();
        setup_test_token(deps.as_mut());
        execute(deps.as_mut(), mock_env(), mock_info("msg1minter", &[]), ExecuteMsg::Mint {
            recipient: "msg1dave".to_string(), amount: Uint128::new(500),
        }).unwrap();
        let dave_bal = BALANCES.load(&deps.storage, &Addr::unchecked("msg1dave")).unwrap();
        assert_eq!(dave_bal, Uint128::new(500));
        let token_info = TOKEN_INFO.load(&deps.storage).unwrap();
        assert_eq!(token_info.total_supply, Uint128::new(2500));
    }

    #[test]
    fn test_unauthorized_mint() {
        let mut deps = mock_dependencies();
        setup_test_token(deps.as_mut());
        let err = execute(deps.as_mut(), mock_env(), mock_info("msg1not_minter", &[]), ExecuteMsg::Mint {
            recipient: "msg1dave".to_string(), amount: Uint128::new(500),
        }).unwrap_err();
        assert_eq!(err, ContractError::Unauthorized {});
    }

    #[test]
    fn test_cap_exceeded() {
        let mut deps = mock_dependencies();
        setup_test_token(deps.as_mut());
        let err = execute(deps.as_mut(), mock_env(), mock_info("msg1minter", &[]), ExecuteMsg::Mint {
            recipient: "msg1dave".to_string(), amount: Uint128::new(100000),
        }).unwrap_err();
        assert!(matches!(err, ContractError::CapExceeded { .. }));
    }

    #[test]
    fn test_zero_amount_transfer_fails() {
        let mut deps = mock_dependencies();
        setup_test_token(deps.as_mut());
        let err = execute(deps.as_mut(), mock_env(), mock_info("msg1alice", &[]), ExecuteMsg::Transfer {
            recipient: "msg1bob".to_string(), amount: Uint128::zero(),
        }).unwrap_err();
        assert_eq!(err, ContractError::ZeroAmount {});
    }

    #[test]
    fn test_approve_expired() {
        let mut deps = mock_dependencies();
        let mut env = mock_env();
        env.block.height = 100;
        setup_test_token(deps.as_mut());
        execute(deps.as_mut(), env.clone(), mock_info("msg1alice", &[]), ExecuteMsg::Approve {
            spender: "msg1bob".to_string(), amount: Uint128::new(100), expires: Some(Expiration::AtHeight(150)),
        }).unwrap();
        env.block.height = 200;
        let err = execute(deps.as_mut(), env, mock_info("msg1bob", &[]), ExecuteMsg::TransferFrom {
            owner: "msg1alice".to_string(), recipient: "msg1charlie".to_string(), amount: Uint128::new(50),
        }).unwrap_err();
        assert_eq!(err, ContractError::Expired {});
    }

    #[test]
    fn test_increase_decrease_allowance() {
        let mut deps = mock_dependencies();
        let env = mock_env();
        setup_test_token(deps.as_mut());
        let info = mock_info("msg1alice", &[]);
        execute(deps.as_mut(), env.clone(), info.clone(), ExecuteMsg::IncreaseAllowance {
            spender: "msg1bob".to_string(), amount: Uint128::new(100), expires: None,
        }).unwrap();
        let allowance = ALLOWANCES.load(&deps.storage, (&Addr::unchecked("msg1alice"), &Addr::unchecked("msg1bob"))).unwrap();
        assert_eq!(allowance.allowance, Uint128::new(100));
        execute(deps.as_mut(), env, info, ExecuteMsg::DecreaseAllowance {
            spender: "msg1bob".to_string(), amount: Uint128::new(30), expires: None,
        }).unwrap();
        let allowance = ALLOWANCES.load(&deps.storage, (&Addr::unchecked("msg1alice"), &Addr::unchecked("msg1bob"))).unwrap();
        assert_eq!(allowance.allowance, Uint128::new(70));
    }

    #[test]
    fn test_query_balance() {
        let mut deps = mock_dependencies();
        setup_test_token(deps.as_mut());
        let bin = query(deps.as_ref(), mock_env(), QueryMsg::Balance { address: "msg1alice".to_string() }).unwrap();
        let res: BalanceResponse = from_binary(&bin).unwrap();
        assert_eq!(res.balance, Uint128::new(1000));
    }

    #[test]
    fn test_query_token_info() {
        let mut deps = mock_dependencies();
        setup_test_token(deps.as_mut());
        let bin = query(deps.as_ref(), mock_env(), QueryMsg::TokenInfo {}).unwrap();
        let res: TokenInfoResponse = from_binary(&bin).unwrap();
        assert_eq!(res.name, "TestToken");
        assert_eq!(res.symbol, "TST");
        assert_eq!(res.decimals, 18);
        assert_eq!(res.total_supply, Uint128::new(2000));
    }
}

2.8 src/lib.rs

pub mod contract;
pub mod error;
pub mod msg;
pub mod state;
pub mod allowances;
pub mod enumerable;
pub mod marketing;
pub use crate::error::ContractError;

2.9 src/enumerable.rs

use cosmwasm_std::{Deps, StdResult, Storage, Addr, Order};
use crate::state::{BALANCES, ALLOWANCES, Allowance};
use crate::msg::{AllAccountsResponse, AllAllowancesResponse, AllowanceInfo};

pub fn query_all_accounts(storage: &dyn Storage, start_after: Option<Addr>, limit: usize) -> StdResult<AllAccountsResponse> {
    let accounts: StdResult<Vec<_>> = BALANCES
        .keys(storage, None, None, Order::Ascending)
        .filter_map(|r| r.ok())
        .skip(start_after.map(|s| s.as_str().len()).unwrap_or(0))
        .take(limit)
        .map(|a| Ok(a.to_string()))
        .collect();
    Ok(AllAccountsResponse { accounts: accounts? })
}

pub fn query_all_allowances(storage: &dyn Storage, owner: &Addr, start_after: Option<Addr>, limit: usize) -> StdResult<AllAllowancesResponse> {
    let allowances: StdResult<Vec<_>> = ALLOWANCES
        .prefix(owner)
        .range(storage, None, None, Order::Ascending)
        .skip(start_after.map(|s| s.as_str().len()).unwrap_or(0))
        .take(limit)
        .map(|r| {
            let (spender, allowance) = r?;
            Ok(AllowanceInfo { owner: owner.to_string(), spender: spender.to_string(), allowance: allowance.allowance, expires: allowance.expires })
        })
        .collect();
    Ok(AllAllowancesResponse { allowances: allowances? })
}

2.10 src/marketing.rs

use cosmwasm_std::{Deps, DepsMut, MessageInfo, Response, StdResult, Binary};
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::error::ContractError;
use crate::msg::{Logo, LogoInfo, MarketingInfoResponse};

const LOGO_SIZE_CAP: u64 = 5 * 1024;
const LOGO_URL_CAP: u64 = 2048;

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MarketingInfo {
    pub project: Option<String>,
    pub description: Option<String>,
    pub marketing: Option<String>,
    pub logo: Option<LogoInfo>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum LogoStore { Url(String), Embedded(Binary) }

pub const MARKETING_INFO: Item<MarketingInfo> = Item::new("marketing_info");
pub const LOGO: Item<LogoStore> = Item::new("logo");

pub fn set_marketing(deps: DepsMut, info: MessageInfo, project: Option<String>, description: Option<String>, marketing: Option<String>) -> Result<Response, ContractError> {
    let mut marketing_info = MARKETING_INFO.may_load(deps.storage)?.unwrap_or(MarketingInfo {
        project: None, description: None, marketing: None, logo: None,
    });
    if marketing_info.marketing.is_some() {
        if Some(info.sender.to_string()) != marketing_info.marketing {
            return Err(ContractError::Unauthorized {});
        }
    }
    if let Some(project) = project { marketing_info.project = Some(project); }
    if let Some(description) = description { marketing_info.description = Some(description); }
    if let Some(marketing) = marketing { marketing_info.marketing = Some(marketing); }
    MARKETING_INFO.save(deps.storage, &marketing_info)?;
    Ok(Response::new().add_attribute("action", "set_marketing"))
}

pub fn upload_logo(deps: DepsMut, info: MessageInfo, logo: Logo) -> Result<Response, ContractError> {
    let marketing_info = MARKETING_INFO.may_load(deps.storage)?.unwrap_or(MarketingInfo {
        project: None, description: None, marketing: None, logo: None,
    });
    if let Some(ref marketing) = marketing_info.marketing {
        if info.sender.to_string() != *marketing { return Err(ContractError::Unauthorized {}); }
    }
    match logo {
        Logo::Url(url) => {
            if url.len() as u64 > LOGO_URL_CAP { return Err(ContractError::Std(cosmwasm_std::StdError::generic_err("URL too long"))); }
            LOGO.save(deps.storage, &LogoStore::Url(url.clone()))?;
        }
        Logo::Embedded(binary) => {
            if binary.len() as u64 > LOGO_SIZE_CAP { return Err(ContractError::Std(cosmwasm_std::StdError::generic_err("Logo too large"))); }
            LOGO.save(deps.storage, &LogoStore::Embedded(binary.clone()))?;
        }
    }
    Ok(Response::new().add_attribute("action", "upload_logo"))
}

pub fn query_marketing_info(deps: Deps) -> StdResult<MarketingInfoResponse> {
    let info = MARKETING_INFO.may_load(deps.storage)?.unwrap_or(MarketingInfo {
        project: None, description: None, marketing: None, logo: None,
    });
    Ok(MarketingInfoResponse { project: info.project, description: info.description, marketing: info.marketing, logo: info.logo })
}

pub fn query_download_logo(deps: Deps) -> StdResult<LogoInfo> {
    let logo = LOGO.may_load(deps.storage)?;
    match logo {
        Some(LogoStore::Url(url)) => Ok(LogoInfo::Url(url)),
        Some(LogoStore::Embedded(_)) => Ok(LogoInfo::Embedded),
        None => Err(cosmwasm_std::StdError::not_found("logo")),
    }
}

2.11 examples/schema.rs

use cosmwasm_schema::write_api;
use cw20_base::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};

fn main() {
    write_api! {
        instantiate: InstantiateMsg,
        execute: ExecuteMsg,
        query: QueryMsg,
    }
}

3. CW20 工厂合约

3.1 概述

工厂合约(Factory Contract)用于一键部署新的 CW20 代币。用户只需调用一次 create_token,合约自动实例化新的 cw20_base 实例并追踪所有已创建的代币。

3.2 项目结构

token-factory/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── contract.rs
│   ├── msg.rs
│   ├── state.rs
│   └── error.rs
├── examples/
│   └── schema.rs
├── tests/
│   └── integration.rs
└── schema/

3.3 Cargo.toml

[package]
name = "token-factory"
version = "1.0.0"
edition = "2021"
description = "MSG Chain CW20 Token Factory - deploy new tokens with one call"

[lib]
crate-type = ["cdylib", "rlib"]

[profile.release]
opt-level = 3
debug = false
rpath = false
lto = true
debug-assertions = false
codegen-units = 1
panic = "abort"
incremental = false
overflow-checks = true

[dependencies]
cosmwasm-std = "1.5"
cw-storage-plus = "1.2"
cw2 = "1.1"
cw20 = "1.1"
cw-utils = "1.0"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
uint = "0.9"

[dev-dependencies]
cw-multi-test = "0.18"
anyhow = "1.0"

3.4 src/state.rs

use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Coin, Uint128};
use cw_storage_plus::{Item, Map};

#[cw_serde]
pub struct Config {
    pub token_code_id: u64,
    pub creation_fee: Option<Coin>,
    pub owner: Addr,
    pub fee_collector: Option<Addr>,
}

#[cw_serde]
pub struct TokenInfo {
    pub contract_addr: Addr,
    pub name: String,
    pub symbol: String,
    pub decimals: u8,
    pub creator: Addr,
    pub created_at: u64,
    pub total_supply: Uint128,
    pub is_mintable: bool,
}

pub const CONFIG: Item<Config> = Item::new("config");
pub const TOKEN_LIST: Map<u64, TokenInfo> = Map::new("token_list");
pub const TOKEN_COUNT: Item<u64> = Item::new("token_count");
pub const TOKEN_BY_ADDRESS: Map<&Addr, u64> = Map::new("token_by_addr");
pub const CREATOR_TOKENS: Map<(&Addr, u64), bool> = Map::new("creator_tokens");

3.5 src/msg.rs

use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Coin, Uint128};

#[cw_serde]
pub struct InstantiateMsg {
    pub token_code_id: u64,
    pub creation_fee: Option<Coin>,
    pub fee_collector: Option<String>,
}

#[cw_serde]
pub enum ExecuteMsg {
    CreateToken {
        name: String,
        symbol: String,
        decimals: u8,
        initial_balances: Vec<Cw20Coin>,
        minter: Option<String>,
        cap: Option<Uint128>,
    },
    UpdateConfig {
        token_code_id: Option<u64>,
        creation_fee: Option<Option<Coin>>,
        fee_collector: Option<Option<String>>,
        owner: Option<String>,
    },
    WithdrawFees {
        amount: Option<Coin>,
        recipient: Option<String>,
    },
}

#[cw_serde]
pub struct Cw20Coin {
    pub address: String,
    pub amount: Uint128,
}

#[cw_serde]
pub enum QueryMsg {
    Config {},
    ListTokens { start_after: Option<u64>, limit: Option<u32> },
    TokenCount {},
    GetToken { contract_addr: String },
    GetTokensByCreator { creator: String, start_after: Option<u64>, limit: Option<u32> },
    FeeBalance {},
}

#[cw_serde]
pub struct ConfigResponse {
    pub token_code_id: u64,
    pub creation_fee: Option<Coin>,
    pub owner: String,
    pub fee_collector: Option<String>,
}

#[cw_serde]
pub struct ListTokensResponse { pub tokens: Vec<TokenInfoResponse> }

#[cw_serde]
pub struct TokenInfoResponse {
    pub index: u64,
    pub contract_addr: String,
    pub name: String,
    pub symbol: String,
    pub decimals: u8,
    pub creator: String,
    pub created_at: u64,
    pub total_supply: Uint128,
    pub is_mintable: bool,
}

#[cw_serde]
pub struct TokenCountResponse { pub count: u64 }
#[cw_serde]
pub struct FeeBalanceResponse { pub balance: Vec<Coin> }

3.6 src/error.rs

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

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
    #[error("{0}")] Std(#[from] StdError),
    #[error("Unauthorized")] Unauthorized {},
    #[error("Insufficient fee: provided {provided}, required {required}")]
    InsufficientFee { provided: String, required: String },
    #[error("Token code ID not set")] TokenCodeIdNotSet {},
    #[error("Invalid reply from token instantiation")] InvalidReply {},
    #[error("Token already registered")] TokenAlreadyRegistered {},
    #[error("No fees to withdraw")] NoFees {},
    #[error("Invalid decimals: must be between 0 and 18")] InvalidDecimals {},
    #[error("Token not found")] TokenNotFound {},
}

3.7 src/contract.rs

use cosmwasm_std::{
    entry_point, to_binary, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut,
    Env, MessageInfo, Order, Reply, Response, StdError, StdResult, SubMsg,
    Uint128, WasmMsg,
};
use cw2::set_contract_version;
use cw20::MinterResponse;

use crate::error::ContractError;
use crate::msg::{
    ConfigResponse, Cw20Coin, ExecuteMsg, FeeBalanceResponse,
    InstantiateMsg, ListTokensResponse, QueryMsg, TokenCountResponse,
    TokenInfoResponse,
};
use crate::state::{
    Config, TokenInfo, CONFIG, CREATOR_TOKENS, TOKEN_BY_ADDRESS,
    TOKEN_COUNT, TOKEN_LIST,
};

const CONTRACT_NAME: &str = "cw20-token-factory";
const CONTRACT_VERSION: &str = "1.0.0";
const REPLY_CREATE_TOKEN: u64 = 1;

#[entry_point]
pub fn instantiate(deps: DepsMut, _env: Env, info: MessageInfo, msg: InstantiateMsg) -> StdResult<Response> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
    let fee_collector = match msg.fee_collector {
        Some(addr) => Some(deps.api.addr_validate(&addr)?),
        None => None,
    };
    let config = Config {
        token_code_id: msg.token_code_id,
        creation_fee: msg.creation_fee,
        owner: info.sender,
        fee_collector,
    };
    CONFIG.save(deps.storage, &config)?;
    TOKEN_COUNT.save(deps.storage, &0u64)?;
    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("token_code_id", config.token_code_id.to_string())
        .add_attribute("owner", info.sender.as_str()))
}

#[entry_point]
pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::CreateToken { name, symbol, decimals, initial_balances, minter, cap } => {
            execute_create_token(deps, env, info, name, symbol, decimals, initial_balances, minter, cap)
        }
        ExecuteMsg::UpdateConfig { token_code_id, creation_fee, fee_collector, owner } => {
            execute_update_config(deps, info, token_code_id, creation_fee, fee_collector, owner)
        }
        ExecuteMsg::WithdrawFees { amount, recipient } => {
            execute_withdraw_fees(deps, env, info, amount, recipient)
        }
    }
}

pub fn execute_create_token(
    deps: DepsMut, env: Env, info: MessageInfo,
    name: String, symbol: String, decimals: u8,
    initial_balances: Vec<Cw20Coin>, minter: Option<String>, cap: Option<Uint128>,
) -> Result<Response, ContractError> {
    if decimals > 18 { return Err(ContractError::InvalidDecimals {}); }
    let config = CONFIG.load(deps.storage)?;
    if let Some(ref fee) = config.creation_fee {
        let provided = info.funds.iter()
            .find(|c| c.denom == fee.denom)
            .map(|c| c.amount)
            .unwrap_or(Uint128::zero());
        if provided < fee.amount {
            return Err(ContractError::InsufficientFee {
                provided: format!("{}{}", provided, fee.denom),
                required: format!("{}{}", fee.amount, fee.denom),
            });
        }
    }
    let cw20_init_msg = cw20_base::msg::InstantiateMsg {
        name: name.clone(),
        symbol: symbol.clone(),
        decimals,
        initial_balances: initial_balances.into_iter()
            .map(|c| cw20_base::msg::Cw20Coin { address: c.address, amount: c.amount })
            .collect(),
        mint: minter.map(|m| MinterResponse { minter: m, cap }),
        marketing: None,
    };
    let label = format!("CW20-{}-{}", name, symbol);
    let instantiate_msg = WasmMsg::Instantiate {
        admin: Some(config.owner.to_string()),
        code_id: config.token_code_id,
        msg: to_binary(&cw20_init_msg)?,
        funds: vec![],
        label,
    };
    cw_storage_plus::Item::<PendingTokenInfo>::new("pending_token").save(
        deps.storage,
        &PendingTokenInfo {
            name, symbol, decimals,
            creator: info.sender,
            created_at: env.block.time.seconds(),
            is_mintable: minter.is_some(),
        },
    )?;
    Ok(Response::new()
        .add_submessage(SubMsg::reply_on_success(instantiate_msg, REPLY_CREATE_TOKEN))
        .add_attribute("method", "create_token")
        .add_attribute("name", &name)
        .add_attribute("symbol", &symbol))
}

fn execute_update_config(
    deps: DepsMut, info: MessageInfo,
    token_code_id: Option<u64>, creation_fee: Option<Option<Coin>>,
    fee_collector: Option<Option<String>>, new_owner: Option<String>,
) -> Result<Response, ContractError> {
    let mut config = CONFIG.load(deps.storage)?;
    if info.sender != config.owner { return Err(ContractError::Unauthorized {}); }
    if let Some(code_id) = token_code_id { config.token_code_id = code_id; }
    if let Some(fee) = creation_fee { config.creation_fee = fee; }
    if let Some(collector) = fee_collector {
        config.fee_collector = match collector {
            Some(addr) => Some(deps.api.addr_validate(&addr)?),
            None => None,
        };
    }
    if let Some(owner) = new_owner { config.owner = deps.api.addr_validate(&owner)?; }
    CONFIG.save(deps.storage, &config)?;
    Ok(Response::new().add_attribute("method", "update_config"))
}

fn execute_withdraw_fees(deps: DepsMut, env: Env, info: MessageInfo, amount: Option<Coin>, recipient: Option<String>) -> Result<Response, ContractError> {
    let config = CONFIG.load(deps.storage)?;
    let is_authorized = info.sender == config.owner || Some(info.sender.clone()) == config.fee_collector;
    if !is_authorized { return Err(ContractError::Unauthorized {}); }
    let recipient = match recipient {
        Some(addr) => deps.api.addr_validate(&addr)?,
        None => config.fee_collector.unwrap_or(config.owner),
    };
    let balance = deps.querier.query_all_balances(&env.contract.address)?;
    if balance.is_empty() { return Err(ContractError::NoFees {}); }
    let send_coins = match amount {
        Some(coin) => vec![coin],
        None => balance,
    };
    let send_msg = CosmosMsg::Bank(BankMsg::Send { to_address: recipient.to_string(), assets: send_coins.clone() });
    Ok(Response::new()
        .add_message(send_msg)
        .add_attribute("method", "withdraw_fees"))
}

#[cw_serde]
pub struct PendingTokenInfo {
    pub name: String, pub symbol: String, pub decimals: u8,
    pub creator: Addr, pub created_at: u64, pub is_mintable: bool,
}

#[entry_point]
pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
    match msg.id {
        REPLY_CREATE_TOKEN => handle_create_token_reply(deps, msg),
        _ => Err(ContractError::InvalidReply {}),
    }
}

fn handle_create_token_reply(deps: DepsMut, msg: Reply) -> Result<Response, ContractError> {
    let token_contract = msg.result.into_result()
        .map_err(|e| ContractError::Std(StdError::generic_err(e)))?
        .get_contract_address()
        .ok_or(ContractError::InvalidReply {})?;
    let token_addr = deps.api.addr_validate(&token_contract)?;
    if TOKEN_BY_ADDRESS.may_load(deps.storage, &token_addr)?.is_some() {
        return Err(ContractError::TokenAlreadyRegistered {});
    }
    let pending: PendingTokenInfo = cw_storage_plus::Item::new("pending_token").load(deps.storage)?;
    let token_info: cw20::TokenInfoResponse = deps.querier.query_wasm_smart(
        &token_addr, &cw20::Cw20QueryMsg::TokenInfo {}
    )?;
    let index = TOKEN_COUNT.load(deps.storage)?;
    let record = TokenInfo {
        contract_addr: token_addr.clone(), name: pending.name, symbol: pending.symbol,
        decimals: pending.decimals, creator: pending.creator.clone(),
        created_at: pending.created_at, total_supply: token_info.total_supply,
        is_mintable: pending.is_mintable,
    };
    TOKEN_LIST.save(deps.storage, &index, &record)?;
    TOKEN_BY_ADDRESS.save(deps.storage, &token_addr, &index)?;
    CREATOR_TOKENS.save(deps.storage, (&pending.creator, index), &true)?;
    TOKEN_COUNT.save(deps.storage, &(index + 1))?;
    Ok(Response::new()
        .add_attribute("method", "handle_create_token_reply")
        .add_attribute("token_address", token_contract)
        .add_attribute("token_index", index.to_string()))
}

#[entry_point]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::Config {} => to_binary(&query_config(deps)?),
        QueryMsg::ListTokens { start_after, limit } => to_binary(&query_list_tokens(deps, start_after, limit)?),
        QueryMsg::TokenCount {} => to_binary(&query_token_count(deps)?),
        QueryMsg::GetToken { contract_addr } => to_binary(&query_get_token(deps, contract_addr)?),
        QueryMsg::GetTokensByCreator { creator, start_after, limit } => to_binary(&query_tokens_by_creator(deps, creator, start_after, limit)?),
        QueryMsg::FeeBalance {} => to_binary(&query_fee_balance(deps, env)?),
    }
}

fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
    let config = CONFIG.load(deps.storage)?;
    Ok(ConfigResponse {
        token_code_id: config.token_code_id,
        creation_fee: config.creation_fee,
        owner: config.owner.to_string(),
        fee_collector: config.fee_collector.map(|a| a.to_string()),
    })
}

fn query_list_tokens(deps: Deps, start_after: Option<u64>, limit: Option<u32>) -> StdResult<ListTokensResponse> {
    let limit = limit.unwrap_or(30).min(100) as usize;
    let start = start_after.unwrap_or(0);
    let tokens: StdResult<Vec<_>> = TOKEN_LIST
        .range(deps.storage, Some(start.into()), None, Order::Ascending)
        .take(limit)
        .map(|r| {
            let (index, info) = r?;
            Ok(TokenInfoResponse {
                index, contract_addr: info.contract_addr.to_string(),
                name: info.name, symbol: info.symbol, decimals: info.decimals,
                creator: info.creator.to_string(), created_at: info.created_at,
                total_supply: info.total_supply, is_mintable: info.is_mintable,
            })
        })
        .collect();
    Ok(ListTokensResponse { tokens: tokens? })
}

fn query_token_count(deps: Deps) -> StdResult<TokenCountResponse> {
    Ok(TokenCountResponse { count: TOKEN_COUNT.load(deps.storage)? })
}

fn query_get_token(deps: Deps, contract_addr: String) -> StdResult<TokenInfoResponse> {
    let addr = deps.api.addr_validate(&contract_addr)?;
    let index = TOKEN_BY_ADDRESS.may_load(deps.storage, &addr)?
        .ok_or_else(|| StdError::not_found("Token not found"))?;
    let info = TOKEN_LIST.load(deps.storage, &index)?;
    Ok(TokenInfoResponse {
        index, contract_addr: info.contract_addr.to_string(),
        name: info.name, symbol: info.symbol, decimals: info.decimals,
        creator: info.creator.to_string(), created_at: info.created_at,
        total_supply: info.total_supply, is_mintable: info.is_mintable,
    })
}

fn query_tokens_by_creator(deps: Deps, creator: String, start_after: Option<u64>, limit: Option<u32>) -> StdResult<ListTokensResponse> {
    let creator_addr = deps.api.addr_validate(&creator)?;
    let limit = limit.unwrap_or(30).min(100) as usize;
    let start = start_after.unwrap_or(0);
    let tokens: StdResult<Vec<_>> = CREATOR_TOKENS
        .prefix(&creator_addr)
        .range(deps.storage, Some(start.into()), None, Order::Ascending)
        .take(limit)
        .map(|r| {
            let (index, _) = r?;
            let info = TOKEN_LIST.load(deps.storage, &index)?;
            Ok(TokenInfoResponse {
                index, contract_addr: info.contract_addr.to_string(),
                name: info.name, symbol: info.symbol, decimals: info.decimals,
                creator: info.creator.to_string(), created_at: info.created_at,
                total_supply: info.total_supply, is_mintable: info.is_mintable,
            })
        })
        .collect();
    Ok(ListTokensResponse { tokens: tokens? })
}

fn query_fee_balance(deps: Deps, env: Env) -> StdResult<FeeBalanceResponse> {
    let config = CONFIG.load(deps.storage)?;
    let fee_denom = match &config.creation_fee {
        Some(fee) => fee.denom.clone(),
        None => "umsg".to_string(),
    };
    let balance = deps.querier.query_balance(&env.contract.address, &fee_denom)?;
    Ok(FeeBalanceResponse { balance: vec![balance] })
}

3.8 src/lib.rs

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

4. 代币部署

4.1 使用 msg-chain-devkit CLI

4.1.1 编译 cw20_base 合约

cd contracts/cosmwasm/all/cw20-base
cargo build --target wasm32-unknown-unknown --release
cd ../..

# 使用 devkit 编译所有合约
msg-devkit compile

# 优化 WASM
wasm-opt -Os contracts/cosmwasm/all/cw20-base/target/wasm32-unknown-unknown/release/cw20_base.wasm \
  -o artifacts/cw20_base.wasm

4.1.2 存储合约代码

# 存储 cw20_base.wasm
CODE_ID=$(msgd tx wasm store artifacts/cw20_base.wasm \
  --from my-key \
  --gas auto --gas-adjustment 1.3 \
  --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 \
  --node tcp://localhost:26657 \
  -y --output json | jq -r '.logs[0].events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value')

echo "cw20_base Code ID: $CODE_ID"

# 验证
msgd query wasm code $CODE_ID --node tcp://localhost:26657

4.1.3 直接部署 CW20 代币

CODE_ID=1  # 上一步获取的 Code ID

MSG='{"name":"MyToken","symbol":"MTK","decimals":18,"initial_balances":[{"address":"msg1alice","amount":"1000000000000000000000000"}],"mint":{"minter":"msg1minter","cap":"10000000000000000000000000000"}}'

msgd tx wasm instantiate $CODE_ID "$MSG" \
  --label "MyToken-v1" \
  --admin $(msgd keys show my-key -a) \
  --from my-key \
  --gas auto --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 \
  --node tcp://localhost:26657 -y

# 使用 devkit
msg-devkit deploy \
  --network local \
  --contract cw20_base \
  --label "MyToken-v1" \
  --msg "$MSG"

4.1.4 部署 Factory 合约

# 先存储 factory.wasm
FACTORY_CODE_ID=$(msgd tx wasm store artifacts/token_factory.wasm \
  --from my-key --gas auto --gas-adjustment 1.3 \
  --gas-prices 1000000000attoMSG --chain-id msg-chain-1 -y --output json \
  | jq -r '.logs[0].events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value')

# 实例化 Factory
TOKEN_CODE_ID=1  # cw20_base 的 Code ID

MSG='{"token_code_id":1,"creation_fee":{"denom":"umsg","amount":"1000000000000000000"},"fee_collector":"msg1fee_collector"}'

msgd tx wasm instantiate $FACTORY_CODE_ID "$MSG" \
  --label "cw20-token-factory-v1" \
  --admin $(msgd keys show my-key -a) \
  --from my-key \
  --gas auto --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 -y

# 通过 devkit
msg-devkit deploy \
  --network local \
  --contract token-factory \
  --admin $(msgd keys show my-key -a) \
  --label "cw20-factory-v1" \
  --msg "$MSG"

4.1.5 通过 Factory 创建代币

FACTORY_ADDR="msg1factory_address_here"

MSG='{"create_token":{"name":"FactoryToken","symbol":"FTK","decimals":18,"initial_balances":[{"address":"msg1recipient","amount":"1000000000000000000000000"}],"minter":"msg1minter","cap":"10000000000000000000000000000"}}'

# 如果需要创建费用
FEE='--amount 1000000000000000000umsg'

msgd tx wasm execute $FACTORY_ADDR "$MSG" \
  --from my-key \
  --gas auto --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 -y $FEE

# 查询工厂状态
msgd query wasm contract-state smart $FACTORY_ADDR '{"token_count":{}}'
msgd query wasm contract-state smart $FACTORY_ADDR '{"list_tokens":{"limit":10}}'

4.2 使用 TypeScript / CosmJS

4.2.1 客户端设置

import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { GasPrice } from "@cosmjs/stargate";

async function setupClient(): Promise<{ client: SigningCosmWasmClient; address: string }> {
  const mnemonic = process.env.MSG_MNEMONIC || "";
  const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: "msg" });
  const client = await SigningCosmWasmClient.connectWithSigner(
    "http://localhost:26657", wallet,
    { gasPrice: GasPrice.fromString("1000000000attoMSG") }
  );
  const [account] = await wallet.getAccounts();
  return { client, address: account.address };
}

4.2.2 部署 CW20 代币(TypeScript)

interface Cw20Coin { address: string; amount: string }
interface Cw20InstantiateMsg {
  name: string; symbol: string; decimals: number;
  initial_balances: Cw20Coin[];
  mint: { minter: string; cap?: string } | null;
}

async function deployCw20Token(
  client: SigningCosmWasmClient, deployer: string, codeId: number
): Promise<string> {
  const msg: Cw20InstantiateMsg = {
    name: "MyToken", symbol: "MTK", decimals: 18,
    initial_balances: [{ address: deployer, amount: "1000000000000000000000000" }],
    mint: { minter: "msg1minter", cap: "10000000000000000000000000000" },
  };
  const result = await client.instantiate(deployer, codeId, msg, "MyToken-v1", {
    admin: deployer, label: "MyToken",
  });
  console.log(`Token: ${result.contractAddress}`);
  return result.contractAddress;
}

4.2.3 部署 Factory(TypeScript)

async function deployFactory(
  client: SigningCosmWasmClient, deployer: string,
  factoryCodeId: number, tokenCodeId: number
): Promise<string> {
  const msg = {
    token_code_id: tokenCodeId,
    creation_fee: { denom: "umsg", amount: "1000000000000000000" },
    fee_collector: "msg1collector",
  };
  const result = await client.instantiate(deployer, factoryCodeId, msg, "CW20-Factory", {
    admin: deployer, label: "factory-v1",
  });
  return result.contractAddress;
}

4.2.4 通过 Factory 创建代币并解析事件

async function createTokenViaFactory(
  client: SigningCosmWasmClient, sender: string, factoryAddress: string
): Promise<string> {
  const msg = {
    create_token: {
      name: "FTK", symbol: "FTK", decimals: 18,
      initial_balances: [{ address: sender, amount: "500000000000000000000000" }],
      minter: null, cap: null,
    },
  };
  const funds = [{ denom: "umsg", amount: "1000000000000000000" }];
  const result = await client.execute(sender, factoryAddress, msg, "auto", undefined, funds);

  // 从交易事件中解析代币地址
  const tokenAddress = result.events
    .find((e) => e.type === "wasm")
    ?.attributes.find((a) => a.key === "token_address")?.value;
  return tokenAddress || "";
}

4.2.5 查询代币(TypeScript)

async function queryToken(client: SigningCosmWasmClient, tokenAddress: string, address: string) {
  const info: any = await client.queryContractSmart(tokenAddress, { token_info: {} });
  console.log(`Name: ${info.name}, Symbol: ${info.symbol}, Supply: ${info.total_supply}`);

  const balance: any = await client.queryContractSmart(tokenAddress, { balance: { address } });
  console.log(`Balance of ${address}: ${balance.balance}`);

  const minter: any = await client.queryContractSmart(tokenAddress, { minter: {} });
  console.log(`Minter: ${minter.minter}, Cap: ${minter.cap}`);
}

4.3 使用 Python SDK

4.3.1 客户端设置

import asyncio, json
from cosmipy import CosmWasmClient, Wallet
from cosmipy.models import Coin

async def setup_client():
    wallet = Wallet.from_mnemonic("your mnemonic", prefix="msg")
    client = await CosmWasmClient.connect(
        rpc_url="http://localhost:26657",
        wallet=wallet,
        gas_price=Coin(amount=25000000000000, denom="umsg"),
    )
    return client, wallet.address

4.3.2 部署代币

async def deploy_token(client, deployer: str, code_id: int) -> str:
    msg = {
        "name": "MyToken", "symbol": "MTK", "decimals": 18,
        "initial_balances": [{"address": deployer, "amount": "1000000000000000000000000"}],
        "mint": {"minter": deployer, "cap": "10000000000000000000000000000"}
    }
    result = await client.instantiate(
        sender=deployer, code_id=code_id,
        msg=json.dumps(msg).encode(),
        label="MyToken-v1", admin=deployer,
    )
    print(f"Token deployed: {result.contract_address}")
    return result.contract_address

async def query_token(client, token_address: str):
    info = await client.query_contract_smart(token_address, {"token_info": {}})
    balance = await client.query_contract_smart(
        token_address, {"balance": {"address": "msg1alice"}}
    )
    return info, balance

4.3.3 批量部署

async def batch_deploy(client, deployer: str, code_id: int, count: int):
    addresses = []
    for i in range(count):
        msg = {
            "name": f"BatchToken{i}", "symbol": f"BT{i}", "decimals": 18,
            "initial_balances": [{"address": deployer, "amount": "1000000000000000000000000"}],
            "mint": None,
        }
        result = await client.instantiate(
            sender=deployer, code_id=code_id,
            msg=json.dumps(msg).encode(),
            label=f"BatchToken{i}", admin=deployer,
        )
        addresses.append(result.contract_address)
    return addresses

5. 代币操作指南

5.1 CLI 操作

转账

TOKEN_ADDR="msg1token_address"
# 转账 100 个代币(18位精度 => 100 * 10^18)
msgd tx wasm execute $TOKEN_ADDR \
  '{"transfer":{"recipient":"msg1recipient","amount":"100000000000000000000"}}' \
  --from my-key --gas auto --gas-prices 1000000000attoMSG --chain-id msg-chain-1 -y

授权 + TransferFrom

# Alice 授权 Bob 100 个代币
msgd tx wasm execute $TOKEN_ADDR \
  '{"approve":{"spender":"msg1bob","amount":"100000000000000000000"}}' \
  --from msg1alice --gas auto --gas-prices 1000000000attoMSG --chain-id msg-chain-1 -y

# Bob 从 Alice 转账 50 个给 Charlie
msgd tx wasm execute $TOKEN_ADDR \
  '{"transfer_from":{"owner":"msg1alice","recipient":"msg1charlie","amount":"50000000000000000000"}}' \
  --from msg1bob --gas auto --gas-prices 1000000000attoMSG --chain-id msg-chain-1 -y

铸造

msgd tx wasm execute $TOKEN_ADDR \
  '{"mint":{"recipient":"msg1recipient","amount":"1000000000000000000000000"}}' \
  --from msg1minter --gas auto --gas-prices 1000000000attoMSG --chain-id msg-chain-1 -y

销毁

msgd tx wasm execute $TOKEN_ADDR \
  '{"burn":{"amount":"10000000000000000000"}}' \
  --from my-key --gas auto --gas-prices 1000000000attoMSG --chain-id msg-chain-1 -y

营销信息

msgd tx wasm execute $TOKEN_ADDR \
  '{"set_marketing":{"project":"MyToken","description":"A great token","marketing":"msg1marketing"}}' \
  --from msg1marketing --gas auto --gas-prices 1000000000attoMSG --chain-id msg-chain-1 -y

查询

# 代币信息
msgd query wasm contract-state smart $TOKEN_ADDR '{"token_info":{}}'

# 余额
msgd query wasm contract-state smart $TOKEN_ADDR '{"balance":{"address":"msg1alice"}}'

# 授权
msgd query wasm contract-state smart $TOKEN_ADDR '{"allowance":{"owner":"msg1alice","spender":"msg1bob"}}'

# minter
msgd query wasm contract-state smart $TOKEN_ADDR '{"minter":{}}'

5.2 TypeScript 操作

async function transfer(client: SigningCosmWasmClient, sender: string, token: string, to: string, amount: string) {
  return client.execute(sender, token, { transfer: { recipient: to, amount } }, "auto");
}

async function approve(client: SigningCosmWasmClient, owner: string, token: string, spender: string, amount: string) {
  return client.execute(owner, token, { approve: { spender, amount } }, "auto");
}

async function transferFrom(client: SigningCosmWasmClient, spender: string, token: string, owner: string, to: string, amount: string) {
  return client.execute(spender, token, { transfer_from: { owner, recipient: to, amount } }, "auto");
}

async function mint(client: SigningCosmWasmClient, minter: string, token: string, to: string, amount: string) {
  return client.execute(minter, token, { mint: { recipient: to, amount } }, "auto");
}

async function burn(client: SigningCosmWasmClient, sender: string, token: string, amount: string) {
  return client.execute(sender, token, { burn: { amount } }, "auto");
}

async function send(client: SigningCosmWasmClient, sender: string, token: string, contract: string, amount: string, msg: any) {
  return client.execute(sender, token, { send: { contract, amount, msg: toBase64(msg) } }, "auto");
}

批量转账

async function batchTransfer(
  client: SigningCosmWasmClient, sender: string, tokenAddress: string,
  transfers: Array<{ recipient: string; amount: string }>
) {
  for (const t of transfers) {
    await client.execute(sender, tokenAddress, { transfer: t }, "auto");
  }
}

5.3 Python 操作

async def transfer_tokens(client, sender: str, token_address: str, recipient: str, amount: str):
    msg = {"transfer": {"recipient": recipient, "amount": amount}}
    return await client.execute_contract(
        sender=sender, contract_address=token_address, msg=json.dumps(msg).encode()
    )

async def get_balance(client, token_address: str, address: str) -> int:
    result = await client.query_contract_smart(
        token_address, {"balance": {"address": address}}
    )
    return int(result["balance"])

async def approve_tokens(client, owner: str, token_address: str, spender: str, amount: str):
    msg = {"approve": {"spender": spender, "amount": amount}}
    return await client.execute_contract(
        sender=owner, contract_address=token_address, msg=json.dumps(msg).encode()
    )

async def mint_tokens(client, minter: str, token_address: str, recipient: str, amount: str):
    msg = {"mint": {"recipient": recipient, "amount": amount}}
    return await client.execute_contract(
        sender=minter, contract_address=token_address, msg=json.dumps(msg).encode()
    )

完整操作示例

import asyncio, json
from cosmipy import CosmWasmClient, Wallet
from cosmipy.models import Coin

async def token_operations():
    wallet = Wallet.from_mnemonic("your mnemonic", prefix="msg")
    client = await CosmWasmClient.connect(
        rpc_url="http://localhost:26657", wallet=wallet,
        gas_price=Coin(amount=25000000000000, denom="umsg"),
    )
    sender = wallet.address
    TOKEN = "msg1token_address"

    info = await client.query_contract_smart(TOKEN, {"token_info": {}})
    print(f"Name: {info['name']}, Supply: {info['total_supply']}")

    await client.execute_contract(sender, TOKEN, json.dumps({
        "transfer": {"recipient": "msg1recipient", "amount": "50000000000000000000"}
    }).encode())
    print("Transfer done")

    bal = await client.query_contract_smart(TOKEN, {"balance": {"address": sender}})
    print(f"Balance: {bal['balance']}")

    await client.disconnect()

# asyncio.run(token_operations())

6. CW20 扩展实现

6.1 Snapshot 代币(用于治理)

支持在指定区块高度记录余额快照,用于 DAO 治理投票。

// cw20-snapshot/src/state.rs
use cw_storage_plus::{Item, SnapshotMap};
use cosmwasm_std::{Addr, Uint128};

/// SnapshotMap 自动记录每个键的变更历史
pub const BALANCES_SNAPSHOT: SnapshotMap<&Addr, Uint128> = SnapshotMap::new(
    "balance",        // 主存储
    "balance__chk",   // 检查点
    "balance__chg",   // 变更历史
);

pub const SNAPSHOT_STRATEGY: Item<SnapshotStrategy> = Item::new("snapshot_strategy");

pub enum SnapshotStrategy {
    EveryChange,
    Periodic { block_interval: u64 },
}

/// 查询指定高度的余额快照
pub fn query_balance_at_height(
    storage: &dyn cosmwasm_std::Storage,
    address: &Addr,
    height: u64,
) -> StdResult<Option<Uint128>> {
    BALANCES_SNAPSHOT.may_load_at_height(storage, address, height)
}

/// 查询治理投票权
pub fn query_voting_power_at_height(
    deps: Deps,
    address: String,
    height: u64,
) -> StdResult<Uint128> {
    let addr = deps.api.addr_validate(&address)?;
    Ok(BALANCES_SNAPSHOT
        .may_load_at_height(deps.storage, &addr, height)?
        .unwrap_or_default())
}

部署:将 cw20_base 的 BALANCES 替换为 BALANCES_SNAPSHOT,其余逻辑不变。

6.2 动态转账手续费代币

每笔转账收取一定比例的手续费。

// cw20-fee/src/state.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct FeeConfig {
    pub fee_bps: u16,                    // 手续费基点,如 30 = 0.3%
    pub fee_collector: Addr,             // 手续费接收地址
    pub min_fee: Uint128,                // 最小手续费
    pub max_fee: Option<Uint128>,        // 最大手续费
}

pub const FEE_CONFIG: Item<FeeConfig> = Item::new("fee_config");
// cw20-fee/src/contract.rs
fn apply_transfer_fee(
    storage: &mut dyn cosmwasm_std::Storage,
    sender: &Addr, amount: Uint128,
) -> StdResult<(Uint128, Uint128)> {
    let fee_config = match FEE_CONFIG.may_load(storage)? {
        Some(c) => c, None => return Ok((amount, Uint128::zero())),
    };
    let fee = amount.multiply_ratio(u128::from(fee_config.fee_bps))
        .checked_div(Uint128::from(10000u128))
        .unwrap_or_default();
    let fee = if fee < fee_config.min_fee { fee_config.min_fee }
        else if let Some(max) = fee_config.max_fee { if fee > max { max } else { fee } }
        else { fee };
    if fee.is_zero() { return Ok((amount, Uint128::zero())); }

    let net = amount.checked_sub(fee)?;
    BALANCES.update(storage, sender, |bal| Ok(bal.unwrap_or_default().checked_sub(fee)?))?;
    BALANCES.update(storage, &fee_config.fee_collector, |bal| Ok(bal.unwrap_or_default() + fee))?;
    Ok((net, fee))
}

6.3 可暂停代币(Emergency Stop)

// cw20-pausable/src/state.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PauseInfo {
    pub paused: bool,
    pub pauser: Addr,
    pub paused_at: Option<u64>,
    pub pause_reason: Option<String>,
}

pub const PAUSE_INFO: Item<PauseInfo> = Item::new("pause_info");

fn assert_not_paused(storage: &dyn cosmwasm_std::Storage) -> Result<(), ContractError> {
    let info = PAUSE_INFO.may_load(storage)?.unwrap_or(PauseInfo {
        paused: false, pauser: Addr::unchecked(""), paused_at: None, pause_reason: None,
    });
    if info.paused {
        return Err(ContractError::Std(cosmwasm_std::StdError::generic_err(
            format!("Paused: {}", info.pause_reason.unwrap_or_default())
        )));
    }
    Ok(())
}

pub fn execute_pause(deps: DepsMut, env: Env, info: MessageInfo, reason: Option<String>) -> Result<Response, ContractError> {
    let config = PAUSE_INFO.load(deps.storage)?;
    if info.sender != config.pauser { return Err(ContractError::Unauthorized {}); }
    PAUSE_INFO.save(deps.storage, &PauseInfo {
        paused: true, pauser: config.pauser,
        paused_at: Some(env.block.time.seconds()), pause_reason: reason,
    })?;
    Ok(Response::new().add_attribute("action", "pause"))
}

pub fn execute_unpause(deps: DepsMut, _env: Env, info: MessageInfo) -> Result<Response, ContractError> {
    let config = PAUSE_INFO.load(deps.storage)?;
    if info.sender != config.pauser { return Err(ContractError::Unauthorized {}); }
    PAUSE_INFO.save(deps.storage, &PauseInfo {
        paused: false, pauser: config.pauser, paused_at: None, pause_reason: None,
    })?;
    Ok(Response::new().add_attribute("action", "unpause"))
}

6.4 征税代币(每笔转账自动销毁/收集)

类似 RFI 风格的征税代币,每笔转账自动销毁一定比例并奖励持有者。

// cw20-tax/src/state.rs
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TaxConfig {
    pub burn_bps: u16,           // 销毁比例(基点)
    pub reward_pool_bps: u16,    // 奖励池比例(基点)
    pub reflection_bps: u16,     // 反射比例(基点)
    pub reward_pool: Option<Addr>,
}

impl TaxConfig {
    pub fn total_tax_bps(&self) -> u16 {
        self.burn_bps + self.reward_pool_bps + self.reflection_bps
    }
}

pub const TAX_CONFIG: Item<TaxConfig> = Item::new("tax_config");
// cw20-tax/src/contract.rs
fn apply_tax(storage: &mut dyn cosmwasm_std::Storage, sender: &Addr, amount: Uint128) -> StdResult<Uint128> {
    let config = TAX_CONFIG.load(storage)?;
    let total_tax = config.total_tax_bps();
    if total_tax == 0 { return Ok(amount); }

    let tax = amount.multiply_ratio(u128::from(total_tax))
        .checked_div(Uint128::from(10000u128)).unwrap_or_default();
    if tax.is_zero() { return Ok(amount); }

    let net = amount.checked_sub(tax)?;
    let burn_amt = amount.multiply_ratio(u128::from(config.burn_bps))
        .checked_div(Uint128::from(10000u128)).unwrap_or_default();
    let reward_amt = amount.multiply_ratio(u128::from(config.reward_pool_bps))
        .checked_div(Uint128::from(10000u128)).unwrap_or_default();
    let reflection_amt = tax.checked_sub(burn_amt)?.checked_sub(reward_amt)?;

    // 执行销毁
    if !burn_amt.is_zero() {
        TOKEN_INFO.update(storage, |mut info| -> StdResult<_> {
            info.total_supply = info.total_supply.checked_sub(burn_amt)?;
            Ok(info)
        })?;
    }
    // 发送到奖励池
    if !reward_amt.is_zero() {
        if let Some(ref pool) = config.reward_pool {
            BALANCES.update(storage, pool, |bal| Ok(bal.unwrap_or_default() + reward_amt))?;
        }
    }
    // 反射:按比例增发到所有持有者(简化实现省略)
    // ...
    Ok(net)
}

7. 前端集成

7.1 React Hook — Token Balance

// hooks/useTokenBalance.ts
import { useState, useEffect, useCallback } from "react";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";

interface Props {
  client: SigningCosmWasmClient | null;
  tokenAddress: string;
  address: string;
  refreshInterval?: number;
}

export function useTokenBalance({ client, tokenAddress, address, refreshInterval = 10000 }: Props) {
  const [balance, setBalance] = useState("0");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [tokenInfo, setTokenInfo] = useState<any>(null);

  const fetchBalance = useCallback(async () => {
    if (!client || !tokenAddress || !address) return;
    setLoading(true);
    try {
      const [bal, info] = await Promise.all([
        client.queryContractSmart(tokenAddress, { balance: { address } }),
        tokenInfo ? Promise.resolve(tokenInfo) :
          client.queryContractSmart(tokenAddress, { token_info: {} }),
      ]);
      setBalance(bal.balance);
      if (!tokenInfo) setTokenInfo(info);
    } catch (err) {
      setError((err as Error).message);
    } finally {
      setLoading(false);
    }
  }, [client, tokenAddress, address, tokenInfo]);

  useEffect(() => { fetchBalance(); }, [fetchBalance]);
  useEffect(() => {
    if (refreshInterval > 0) {
      const id = setInterval(fetchBalance, refreshInterval);
      return () => clearInterval(id);
    }
  }, [fetchBalance, refreshInterval]);

  return { balance, tokenInfo, loading, error, refetch: fetchBalance };
}

7.2 React 组件 — Token Transfer

// components/TokenTransfer.tsx
import React, { useState } from "react";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";

interface Props {
  client: SigningCosmWasmClient;
  tokenAddress: string;
  sender: string;
  decimals: number;
  onSuccess?: (txHash: string) => void;
  onError?: (err: Error) => void;
}

export function TokenTransfer({ client, tokenAddress, sender, decimals, onSuccess, onError }: Props) {
  const [recipient, setRecipient] = useState("");
  const [amount, setAmount] = useState("");
  const [sending, setSending] = useState(false);

  const handleTransfer = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!recipient || !amount) return;
    setSending(true);
    try {
      const rawAmount = BigInt(Math.floor(parseFloat(amount) * 10 ** decimals)).toString();
      const result = await client.execute(sender, tokenAddress, {
        transfer: { recipient, amount: rawAmount },
      }, "auto");
      onSuccess?.(result.transactionHash);
      setAmount("");
      setRecipient("");
    } catch (err) {
      onError?.(err as Error);
    } finally {
      setSending(false);
    }
  };

  return (
    <form onSubmit={handleTransfer} className="p-4 bg-white rounded shadow">
      <h3 className="text-lg font-bold mb-3">Transfer Tokens</h3>
      <div className="mb-3">
        <label className="block text-sm font-medium mb-1">Recipient:</label>
        <input type="text" value={recipient} onChange={(e) => setRecipient(e.target.value)}
          placeholder="msg1..." disabled={sending} required
          className="w-full p-2 border rounded" />
      </div>
      <div className="mb-3">
        <label className="block text-sm font-medium mb-1">Amount:</label>
        <input type="number" value={amount} onChange={(e) => setAmount(e.target.value)}
          placeholder="0.0" min="0" step="any" disabled={sending} required
          className="w-full p-2 border rounded" />
      </div>
      <button type="submit" disabled={sending || !recipient || !amount}
        className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50">
        {sending ? "Sending..." : "Transfer"}
      </button>
    </form>
  );
}

7.3 React 组件 — Token Mint

// components/TokenMint.tsx
export function TokenMint({ client, tokenAddress, minter, decimals, onSuccess }: {
  client: SigningCosmWasmClient; tokenAddress: string;
  minter: string; decimals: number;
  onSuccess?: (hash: string) => void;
}) {
  const [recipient, setRecipient] = useState("");
  const [amount, setAmount] = useState("");
  const [minting, setMinting] = useState(false);

  const handleMint = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!recipient || !amount) return;
    setMinting(true);
    try {
      const raw = BigInt(Math.floor(parseFloat(amount) * 10 ** decimals)).toString();
      const res = await client.execute(minter, tokenAddress, {
        mint: { recipient, amount: raw },
      }, "auto");
      onSuccess?.(res.transactionHash);
      setAmount(""); setRecipient("");
    } catch (err) {
      console.error(err);
    } finally {
      setMinting(false);
    }
  };

  return (
    <form onSubmit={handleMint} className="p-4 bg-white rounded shadow">
      <h3 className="text-lg font-bold mb-3">Mint Tokens</h3>
      <div className="mb-3">
        <label className="block text-sm font-medium mb-1">Recipient:</label>
        <input type="text" value={recipient} onChange={(e) => setRecipient(e.target.value)}
          placeholder="msg1..." disabled={minting} required className="w-full p-2 border rounded" />
      </div>
      <div className="mb-3">
        <label className="block text-sm font-medium mb-1">Amount:</label>
        <input type="number" value={amount} onChange={(e) => setAmount(e.target.value)}
          placeholder="0.0" min="0" step="any" disabled={minting} required
          className="w-full p-2 border rounded" />
      </div>
      <button type="submit" disabled={minting || !recipient || !amount}
        className="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 disabled:opacity-50">
        {minting ? "Minting..." : "Mint"}
      </button>
    </form>
  );
}

7.4 Keplr Wallet 集成

// hooks/useKeplr.ts
import { useState, useEffect } from "react";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";

declare global { interface Window { keplr?: any; getOfflineSigner?: any; } }

export function useKeplr() {
  const [client, setClient] = useState<SigningCosmWasmClient | null>(null);
  const [address, setAddress] = useState("");
  const [error, setError] = useState<string | null>(null);

  const connect = async () => {
    if (!window.keplr) { setError("Please install Keplr"); return; }
    try {
      await window.keplr.enable("msg-chain-1");
      const offlineSigner = window.keplr.getOfflineSigner("msg-chain-1");
      const accounts = await offlineSigner.getAccounts();
      setAddress(accounts[0].address);

      const client = await SigningCosmWasmClient.connectWithSigner(
        "https://rpc.mainnet.msgchain.org", offlineSigner,
        { gasPrice: { denom: "umsg", amount: "1000000000" } },
      );
      setClient(client);
    } catch (err) {
      setError((err as Error).message);
    }
  };

  return { client, address, error, connect };
}

8. 安全考虑

8.1 Approve/TransferFrom 重入攻击

风险:经典的 ERC20 重入攻击:Alice 授权 Bob 100 个代币,Bob 调用 transferFrom 消费 50 个,并在回调中再次尝试消费剩余部分。

防护(已在 CW20 标准中内置):

// 正确:先扣 allowance 再转账
ALLOWANCES.update(storage, (&owner, &spender), |allow| {
    let mut a = allow.unwrap_or(default);
    a.allowance = a.allowance.checked_sub(amount)?;  // Checks-Effects
    Ok(a)
})?;
transfer_balance(storage, &owner, &recipient, amount)?;  // Interactions

8.2 Approval Frontrunning

风险:Alice 将授权从 100 改为 50,Bob 看到交易并抢先提交消费原 100 + 新 50。

防护:使用 DecreaseAllowance(原子减少)而不是先设为 0 再设新值。

// ❌ 不安全:竞态窗口
execute_approve(spender, 0);    // 重置
execute_approve(spender, 50);   // 设置新值

// ✅ 安全:原子操作
execute_decrease_allowance(spender, 50);

8.3 初始余额溢出

风险:instantiate 中 initial_balances 的 amount 总和可能溢出 Uint128。

防护:Rust 的 checked_add 和 CosmWasm 的 overflow-checks = true 自动捕获。

let total_supply = msg.initial_balances.iter()
    .try_fold(Uint128::zero(), |acc, c| acc.checked_add(c.amount))?;
// 溢出时会自动抛出错误

8.4 Minter 密钥管理

风险:minter 私钥泄露可能导致无限铸造。

防护:

// 推荐:初始化时设置 cap
let minter = Some(MinterResponse {
    minter: "msg1dao_multisig".to_string(),
    cap: Some(Uint128::new(1_000_000_000_000_000_000_000_000)), // 1M tokens
});

8.5 Checks-Effects-Interactions 模式

// ✅ 正确顺序
fn execute_transfer_from(deps: DepsMut, ..., amount: Uint128) -> Result<Response, ContractError> {
    // 1. Checks: 验证条件
    check_allowance(deps.storage, &owner, &spender, amount, height, time)?;

    // 2. Effects: 更新状态
    ALLOWANCES.update(...)?;

    // 3. Interactions: 发送消息
    transfer_balance(deps.storage, &owner, &recipient, amount)?;

    Ok(Response::new()...)
}

8.6 Expiration 验证

始终检查授权是否过期,防止在过期后继续使用。

if allowance.expires.is_expired(env.block.height, env.block.time.seconds()) {
    return Err(ContractError::Expired {});
}

8.7 其他安全要点

风险 防护
重入攻击 Checks-Effects-Interactions 模式
整数溢出 overflow-checks = true, checked_* 方法
未授权铸造 minter 权限检查 + cap 上限
未授权销毁 仅允许 owner 或 approved spender
精度丢失 使用 Uint128,先乘后除
钓鱼转账 Send 消息强制目标合约处理回调
DOS 循环 设置查询分页 limit(默认 20,最大 100)

8.8 工厂合约安全

// 1. 验证 reply 返回的合约地址
fn handle_create_token_reply(deps: DepsMut, msg: Reply) -> Result<Response, ContractError> {
    let addr = msg.result.into_result()
        .map_err(|e| ContractError::Std(StdError::generic_err(e)))?
        .get_contract_address()
        .ok_or(ContractError::InvalidReply {})?;
    // ✅ 验证地址格式
    let validated = deps.api.addr_validate(&addr)?;
    // ...
}

// 2. 防止代币地址重复注册
if TOKEN_BY_ADDRESS.may_load(deps.storage, &validated)?.is_some() {
    return Err(ContractError::TokenAlreadyRegistered {});
}

// 3. 费用检查使用 checked 操作
let provided = info.funds.iter()
    .find(|c| c.denom == fee.denom)
    .map(|c| c.amount)
    .unwrap_or(Uint128::zero());
if provided < fee.amount {
    return Err(ContractError::InsufficientFee { ... });
}

9. 完整示例项目

9.1 端到端流程:部署 Factory → 创建代币 → 转账

# ===== 阶段 1: 编译和存储合约 =====

# 编译 cw20_base
cd contracts/cosmwasm/all/cw20-base
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
wasm-opt -Os target/wasm32-unknown-unknown/release/cw20_base.wasm -o ../../../../artifacts/cw20_base.wasm

# 编译 token-factory
cd ../../token-factory
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
wasm-opt -Os target/wasm32-unknown-unknown/release/token_factory.wasm -o ../../../../artifacts/token_factory.wasm

cd ../../../..

# ===== 阶段 2: 存储代码 =====

# 存储 cw20_base
CW20_CODE_ID=$(msgd tx wasm store artifacts/cw20_base.wasm \
  --from validator --gas auto --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 -y --output json \
  | jq -r '.logs[0].events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value')

echo "cw20_base code_id: $CW20_CODE_ID"

# 存储 factory
FACTORY_CODE_ID=$(msgd tx wasm store artifacts/token_factory.wasm \
  --from validator --gas auto --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 -y --output json \
  | jq -r '.logs[0].events[] | select(.type=="store_code") | .attributes[] | select(.key=="code_id") | .value')

echo "factory code_id: $FACTORY_CODE_ID"

# ===== 阶段 3: 部署 Factory =====

FACTORY_ADDR=$(msgd tx wasm instantiate $FACTORY_CODE_ID \
  '{"token_code_id":'$CW20_CODE_ID',"creation_fee":{"denom":"umsg","amount":"1000000000000000000"}}' \
  --label "TokenFactory" --admin $(msgd keys show validator -a) \
  --from validator --gas auto --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 -y --output json \
  | jq -r '.logs[0].events[] | select(.type=="instantiate") | .attributes[] | select(.key=="_contract_address") | .value')

echo "Factory: $FACTORY_ADDR"

# ===== 阶段 4: 通过 Factory 创建代币 =====

CREATOR=$(msgd keys show validator -a)

# 创建代币(支付 1 MSG 创建费)
TOKEN_RESULT=$(msgd tx wasm execute $FACTORY_ADDR \
  '{"create_token":{"name":"MyGold","symbol":"GLD","decimals":18,"initial_balances":[{"address":"'$CREATOR'","amount":"1000000000000000000000000"}],"minter":"'$CREATOR'","cap":"10000000000000000000000000000"}}' \
  --from validator --gas auto --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 -y --amount 1000000000000000000umsg --output json)

TOKEN_ADDR=$(echo $TOKEN_RESULT | jq -r '.logs[0].events[] | select(.type=="reply") | .attributes[] | select(.key=="token_address") | .value')

echo "Token: $TOKEN_ADDR"

# ===== 阶段 5: 查询 =====

msgd query wasm contract-state smart $TOKEN_ADDR '{"token_info":{}}'
msgd query wasm contract-state smart $TOKEN_ADDR '{"balance":{"address":"'$CREATOR'"}}'
msgd query wasm contract-state smart $FACTORY_ADDR '{"token_count":{}}'

# ===== 阶段 6: 转账 =====

RECIPIENT="msg1recipient_address"
msgd tx wasm execute $TOKEN_ADDR \
  '{"transfer":{"recipient":"'$RECIPIENT'","amount":"100000000000000000000"}}' \
  --from validator --gas auto --gas-prices 1000000000attoMSG --chain-id msg-chain-1 -y

# 验证转账
msgd query wasm contract-state smart $TOKEN_ADDR '{"balance":{"address":"'$RECIPIENT'"}}'

# ===== 阶段 7: 查询工厂所有代币 =====

msgd query wasm contract-state smart $FACTORY_ADDR '{"list_tokens":{"limit":10}}'

9.2 TypeScript 完整示例

import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { GasPrice } from "@cosmjs/stargate";
import fs from "fs";

async function main() {
  // 1. 设置客户端
  const wallet = await DirectSecp256k1HdWallet.fromMnemonic(
    "test mnemonic", { prefix: "msg" }
  );
  const client = await SigningCosmWasmClient.connectWithSigner(
    "http://localhost:26657", wallet,
    { gasPrice: GasPrice.fromString("1000000000attoMSG") }
  );
  const [account] = await wallet.getAccounts();
  const deployer = account.address;

  // 2. 存储合约
  const cw20Wasm = fs.readFileSync("artifacts/cw20_base.wasm");
  const cw20CodeId = (await client.upload(deployer, cw20Wasm, "auto")).codeId;

  const factoryWasm = fs.readFileSync("artifacts/token_factory.wasm");
  const factoryCodeId = (await client.upload(deployer, factoryWasm, "auto")).codeId;

  console.log(`cw20_base code: ${cw20CodeId}, factory code: ${factoryCodeId}`);

  // 3. 部署 Factory
  const factoryAddr = (await client.instantiate(deployer, factoryCodeId, {
    token_code_id: cw20CodeId,
    creation_fee: { denom: "umsg", amount: "1000000000000000000" },
    fee_collector: null,
  }, "Factory", { admin: deployer, label: "v1" })).contractAddress;

  console.log(`Factory: ${factoryAddr}`);

  // 4. 创建代币
  const result = await client.execute(deployer, factoryAddr, {
    create_token: {
      name: "Gold", symbol: "GLD", decimals: 18,
      initial_balances: [{ address: deployer, amount: "1000000000000000000000000" }],
      minter: deployer, cap: "10000000000000000000000000000",
    },
  }, "auto", undefined, [{ denom: "umsg", amount: "1000000000000000000" }]);

  const tokenAddr = result.events
    .find(e => e.type === "wasm")
    ?.attributes.find(a => a.key === "token_address")?.value || "";

  console.log(`Token: ${tokenAddr}`);

  // 5. 查询
  const info: any = await client.queryContractSmart(tokenAddr, { token_info: {} });
  console.log(`Name: ${info.name}, Supply: ${info.total_supply}`);

  const balance: any = await client.queryContractSmart(tokenAddr, { balance: { address: deployer } });
  console.log(`Balance: ${balance.balance}`);

  // 6. 转账
  await client.execute(deployer, tokenAddr, {
    transfer: { recipient: "msg1recipient", amount: "100000000000000000000" },
  }, "auto");

  const recipientBal: any = await client.queryContractSmart(
    tokenAddr, { balance: { address: "msg1recipient" } }
  );
  console.log(`Recipient balance: ${recipientBal.balance}`);
}

main().catch(console.error);

10. 附录

A. 完整消息 Schema

InstantiateMsg:

{
  "name": "string",
  "symbol": "string",
  "decimals": "uint8 (0-18)",
  "initial_balances": [
    {"address": "string (msg1...)","amount": "string (Uint128)"}
  ],
  "mint": null | {
    "minter": "string",
    "cap": null | "string (Uint128)"
  },
  "marketing": null | {
    "project": null | "string",
    "description": null | "string",
    "marketing": null | "string",
    "logo": null | {"url": "string"} | {"embedded": "Binary (base64)"}
  }
}

ExecuteMsg(枚举,以下之一):

{"transfer": {"recipient": "string", "amount": "string"}}
{"approve":  {"spender": "string", "amount": "string", "expires": null|{"at_height":64}|{"at_time":64}|{"never":{}}}
{"transfer_from": {"owner": "string", "recipient": "string", "amount": "string"}}
{"burn": {"amount": "string"}}
{"mint": {"recipient": "string", "amount": "string"}}
{"send": {"contract": "string", "amount": "string", "msg": "Binary"}}
{"increase_allowance": {"spender": "string", "amount": "string", "expires": ...}}
{"decrease_allowance": {"spender": "string", "amount": "string", "expires": ...}}
{"update_minter": {"new_minter": null|"string"}}
{"set_marketing": {"project": null|"string", "description": null|"string", "marketing": null|"string"}}

QueryMsg(枚举,以下之一):

{"balance": {"address": "string"}}
{"token_info": {}}
{"minter": {}}
{"allowance": {"owner": "string", "spender": "string"}}
{"all_accounts": {"start_after": null|"string", "limit": null|32}}
{"all_allowances": {"owner": "string", "start_after": null|"string", "limit": null|32}}
{"marketing_info": {}}

B. Event 属性

每次执行操作时合约发出以下事件:

操作 Event Type Attributes
Transfer wasm action=transfer, from, to, amount
Approve wasm action=approve, owner, spender, amount
TransferFrom wasm action=transfer_from, from, to, by, amount
Burn wasm action=burn, from, amount
Mint wasm action=mint, to, amount
Send wasm action=send, from, to, amount

C. 精度换算速查表

用户显示数量 原始金额(18位精度)
0.000001 1000000000000
0.001 1000000000000000
0.01 10000000000000000
0.1 100000000000000000
1 1000000000000000000
10 10000000000000000000
100 100000000000000000000
1000 1000000000000000000000

D. Gas 估算参考

操作 估算 Gas 费用(Gas x 1,000,000,000 attoMSG)
Transfer ~150,000 3,750 umsg
Approve ~120,000 3,000 umsg
TransferFrom ~180,000 4,500 umsg
Mint ~160,000 4,000 umsg
Burn ~130,000 3,250 umsg
Factory CreateToken ~400,000 10,000 umsg
Factory Query List ~100,000 2,500 umsg

E. 常见错误排查

错误信息 原因 解决
Overflow: insufficient balance 余额不足 检查发送者余额
Expired 授权已过期 重新授权或设置 Never
Unauthorized 调用者无权限 检查 minter/owner 地址
CapExceeded 铸造超过上限 提高 cap 或等待销毁后再铸
InsufficientFee 创建代币未付费用 附带 --amount 参数
NotMintable 代币不可铸造 初始化时设置 mint 字段
decimals must not exceed 18 精度 > 18 设置 decimals <= 18

本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/

本文档为 MSG Chain 开发者提供 CW20 代币标准、完整实现、工厂合约及操作指南。
相关资源:

  • CW20 规范: https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md
  • CosmWasm 文档: https://docs.cosmwasm.com
  • MSG Chain DevKit: msg-devkit --help