CosmWasm 智能合约设计模式大全
数据来源:MSG Chain 代码库核实
主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。
目录
1. 概述
1.1 为什么需要设计模式
智能合约一旦部署即不可篡改,MSG Chain 上的 CosmWasm 合约同样遵循这一原则。设计模式是经过实战验证的标准化解决方案,帮助开发者避免常见陷阱,降低审计成本,提升合约的可维护性和可升级性。
1.2 CosmWasm vs Solidity 模式差异
| 维度 | CosmWasm | Solidity |
|---|---|---|
| 存储模型 | KV 数据库 (LevelDB/RocksDB) | 256-bit 存储槽 |
| 并发安全 | 单线程执行,无重入担忧 | 需显式处理重入 |
| 合约升级 | MigrateMsg 原生支持 | delegatecall / proxy 模式 |
| 跨合约调用 | SubMsg + Reply 异步模式 | 同步调用 |
| 接口标准 | CW20 / CW721 / CW1 / CW3 等 | ERC20 / ERC721 / ERC1155 |
| 地址格式 | msg1... bech32 |
0x... hex |
1.3 模式分类
本手册将模式分为七大类别:
| 类别 | 核心关注点 | 典型模式 |
|---|---|---|
| 存储模式 | 数据读写效率与可迁移性 | Item, Map, IndexedMap, SnapshotMap, 复合键 |
| 授权模式 | 权限控制与访问管理 | Owner-only, RBAC, Allowance, 多签 |
| 工厂模式 | 合约部署与实例化管理 | Instantiate2, Reply-based 工厂, 收费工厂 |
| 回调模式 | 跨合约异步通信 | SubMsg, Reply, 跨合约查询 |
| 升级模式 | 合约版本演进 | MigrateMsg, 存储迁移, Proxy |
| 经济模式 | 代币经济与资金管理 | 托管, 转账抽税, TWAP, 奖励累积 |
| 安全模式 | 风险控制与应急处理 | Checks-Effects-Interactions, 两步转移, 暂停, 限流 |
1.4 MSG Chain 特定注意事项
- Bech32 前缀: 所有地址以
msg开头,例如msg1qypqxpq9kcre0k8l7d3v9z9j3vz2z9j3vz2z9j - Chain ID:
msg-chain-1 - 原生代币: MSG,精度 18 位
- Gas 费用: 使用 MSG 支付,支持 IBC 代币
- 合约实例化: 支持
Instantiate2确定性地址部署 - IBC 支持: 完整 ICS-20 / ICS-721 支持
2. 存储模式
2.1 Item vs Map vs SnapshotMap vs IndexedMap
CosmWasm 提供了 cw-storage-plus 包,极大地简化了存储操作。以下是四种核心存储原语的选择指南:
| 类型 | 适用场景 | KV 数 | 迭代 | 快照 |
|---|---|---|---|---|
Item |
单值配置 / 全局状态 | 1 | 否 | 否 |
Map |
键值对集合 | N | 是 | 否 |
SnapshotMap |
需要历史查询的 Map | N | 是 | 是 |
IndexedMap |
多维度查询的 Map | N + 索引 | 是 | 否 |
Item — 全局单例存储
use cosmwasm_std::Addr;
use cw_storage_plus::Item;
/// 合约所有者
pub const OWNER: Item<Addr> = Item::new("owner");
/// 全局配置
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
pub admin: Addr,
pub fee_percent: u64,
pub paused: bool,
}
pub const CONFIG: Item<Config> = Item::new("config");
// 写入
pub fn set_config(storage: &mut dyn Storage, config: &Config) -> StdResult<()> {
CONFIG.save(storage, config)
}
// 读取
pub fn get_config(storage: &dyn Storage) -> StdResult<Config> {
CONFIG.load(storage)
}
// 更新(read-modify-write 原子操作)
pub fn update_admin(storage: &mut dyn Storage, new_admin: &Addr) -> StdResult<Addr> {
CONFIG.update(storage, |mut c| {
let old = c.admin.clone();
c.admin = new_admin.clone();
Ok((c, old))
})
}
Map — 键值映射存储
use cw_storage_plus::Map;
/// 用户余额: address -> balance
pub const BALANCES: Map<&Addr, Uint128> = Map::new("balance");
/// 委托关系: (delegator, validator) -> amount
pub const DELEGATIONS: Map<(&Addr, &Addr), Uint128> = Map::new("delegation");
/// 使用复合键存储
pub fn set_balance(
storage: &mut dyn Storage,
owner: &Addr,
amount: Uint128,
) -> StdResult<()> {
BALANCES.save(storage, owner, &amount)
}
pub fn get_balance(storage: &dyn Storage, owner: &Addr) -> StdResult<Uint128> {
BALANCES
.load(storage, owner)
.unwrap_or_default()
}
/// 迭代所有余额(可能消耗大量 Gas)
pub fn all_balances(
storage: &dyn Storage,
start_after: Option<Addr>,
limit: usize,
) -> StdResult<Vec<(Addr, Uint128)>> {
BALANCES
.range(storage, start_after.map(|a| a.as_ref()), None, Order::Ascending)
.take(limit)
.map(|item| {
let (key, balance) = item?;
let addr = Addr::unchecked(String::from_utf8(key)?);
Ok((addr, balance))
})
.collect()
}
SnapshotMap — 带历史快照的 Map
SnapshotMap 保存每个键的变更历史,适用于需要查询历史状态的应用(如 Staking、投票)。
use cw_storage_plus::SnapshotMap;
use cw_utils::Expiration;
/// 带快照的用户余额
pub const SNAPSHOT_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new(
"snap_balance", // 主存储前缀
"snap_balance__h", // 历史存储前缀
"snap_balance__c", // 检查点前缀
);
/// 在区块高度记录快照
pub fn snapshot_balance(
storage: &mut dyn Storage,
owner: &Addr,
height: u64,
) -> StdResult<()> {
let bal = BALANCES.load(storage, owner)?;
SNAPSHOT_BALANCES.save(storage, owner, &bal, height)?;
Ok(())
}
/// 查询指定高度的余额
pub fn query_balance_at(
storage: &dyn Storage,
owner: &Addr,
height: u64,
) -> StdResult<Uint128> {
SNAPSHOT_BALANCES
.may_load_at_height(storage, owner, height)?
.unwrap_or_default()
}
IndexedMap — 多索引查询
IndexedMap 允许你为 Map 创建额外索引,实现多维度查询。
use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex};
/// 代币持有者结构
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TokenOwner {
pub address: Addr,
pub token_id: String,
pub balance: Uint128,
}
/// 索引结构体
pub struct TokenOwnerIndexes<'a> {
/// 按地址索引
pub address: MultiIndex<'a, Addr, TokenOwner, String>,
}
impl<'a> IndexList<TokenOwner> for TokenOwnerIndexes<'a> {
fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<TokenOwner>> + '_> {
let v: Vec<&dyn Index<TokenOwner>> = vec![&self.address];
Box::new(v.into_iter())
}
}
/// 主存储 + 索引
pub fn token_owners<'a>() -> IndexedMap<'a, &'a str, TokenOwner, TokenOwnerIndexes<'a>> {
let indexes = TokenOwnerIndexes {
address: MultiIndex::new(
|d: &TokenOwner| d.address.clone(),
"token_owners", // 主存储前缀
"token_owners_a", // 索引前缀
),
};
IndexedMap::new("token_owners", indexes)
}
// 按 token_id 查询
pub fn get_owner(storage: &dyn Storage, token_id: &str) -> StdResult<TokenOwner> {
token_owners().load(storage, token_id)
}
// 按地址查询所有代币
pub fn get_tokens_by_address(
storage: &dyn Storage,
addr: &Addr,
) -> Vec<TokenOwner> {
token_owners()
.idx
.address
.prefix(addr.clone())
.range(storage, None, None, Order::Ascending)
.map(|r| r.map(|(_, v)| v))
.collect::<StdResult<Vec<_>>>()
.unwrap_or_default()
}
// 保存(自动更新索引)
pub fn save_token_owner(
storage: &mut dyn Storage,
token_id: &str,
owner: &TokenOwner,
) -> StdResult<()> {
token_owners().save(storage, token_id, owner)
}
2.2 复合键实现复杂查询
实际业务中经常需要多维查询。以下展示如何在 MSG Chain 上实现高效的复合键模式。
使用元组复合键
use cw_storage_plus::Map;
/// (委托者, 验证者) -> 委托金额
pub const DELEGATIONS: Map<(&Addr, &Addr), Uint128> = Map::new("del");
/// 按委托者查询所有委托
pub fn get_delegations_by_delegator(
storage: &dyn Storage,
delegator: &Addr,
) -> Vec<(Addr, Uint128)> {
DELEGATIONS
.prefix(delegator)
.range(storage, None, None, Order::Ascending)
.map(|r| r.map(|(v, bal)| (Addr::unchecked(v), bal)))
.collect::<StdResult<Vec<_>>>()
.unwrap_or_default()
}
/// 跨分页查询
pub fn query_delegations_paginated(
storage: &dyn Storage,
delegator: &Addr,
start_after: Option<Addr>,
limit: usize,
) -> StdResult<Vec<(Addr, Uint128)>> {
let start = start_after.map(|a| a.as_ref().to_vec());
DELEGATIONS
.prefix(delegator)
.range(storage, start.as_deref(), None, Order::Ascending)
.take(limit)
.map(|r| {
let (v, bal) = r?;
Ok((Addr::unchecked(String::from_utf8(v)?), bal))
})
.collect()
}
使用字符串拼接复合键
对于超过 2 个维度的复合键,字符串拼接更灵活:
pub fn compose_key(parts: &[&str]) -> String {
parts.join("::")
}
pub fn decompose_key(key: &str) -> Vec<String> {
key.split("::").map(String::from).collect()
}
/// 三级复合键: (池ID, 用户地址, 代币种类)
pub const USER_POSITIONS: Map<String, Uint128> = Map::new("upos");
pub fn save_position(
storage: &mut dyn Storage,
pool_id: &str,
user: &Addr,
token: &str,
amount: Uint128,
) -> StdResult<()> {
let key = compose_key(&[pool_id, user.as_ref(), token]);
USER_POSITIONS.save(storage, key, &amount)
}
pub fn get_all_user_positions_in_pool(
storage: &dyn Storage,
pool_id: &str,
user: &Addr,
) -> Vec<(String, Uint128)> {
let prefix = format!("{}::{}", pool_id, user.as_ref());
USER_POSITIONS
.prefix(&prefix)
.range(storage, None, None, Order::Ascending)
.map(|r| r.map(|(k, v)| (k, v)))
.collect::<StdResult<Vec<_>>>()
.unwrap_or_default()
}
2.3 延迟加载处理大数据集
当合约存储的数据量可能很大时(例如 CW721 的元数据),应该避免在单次查询中加载全部数据。
use cosmwasm_std::Storage;
use cw_storage_plus::Map;
/// 代币元数据(可能很大)
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TokenMetadata {
pub name: Option<String>,
pub description: Option<String>,
pub image: Option<String>,
pub attributes: Vec<Trait>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Trait {
pub trait_type: String,
pub value: String,
}
/// 延迟加载用前缀查询
pub const METADATA: Map<&str, TokenMetadata> = Map::new("meta");
/// 分页查询批量元数据(避免一次加载全部)
pub fn query_metadata_batch(
storage: &dyn Storage,
token_ids: &[String],
) -> Vec<(String, Option<TokenMetadata>)> {
token_ids
.iter()
.map(|id| {
let meta = METADATA.may_load(storage, id.as_str()).unwrap_or(None);
(id.clone(), meta)
})
.collect()
}
/// 流式迭代(使用 Iterator,适合大数据的批处理)
pub fn process_all_metadata<F>(storage: &dyn Storage, mut f: F) -> StdResult<()>
where
F: FnMut(String, TokenMetadata) -> StdResult<()>,
{
for item in METADATA.range(storage, None, None, Order::Ascending) {
let (key_bytes, meta) = item?;
let token_id = String::from_utf8(key_bytes).map_err(|_| {
StdError::generic_err("Invalid UTF-8 in token ID")
})?;
f(token_id, meta)?;
}
Ok(())
}
2.4 版本化存储支持合约升级
合约升级时需要迁移存储结构。版本化存储模式确保平滑过渡。
use cosmwasm_std::Storage;
use cw_storage_plus::Item;
/// 存储版本标记
pub const STORAGE_VERSION: Item<u64> = Item::new("storage_version");
const VERSION: u64 = 2;
/// 迁移前检测版本
pub fn check_version(storage: &dyn Storage) -> StdResult<bool> {
let current = STORAGE_VERSION
.may_load(storage)?
.unwrap_or(0);
Ok(current == VERSION)
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigV1 {
pub owner: Addr,
pub fee: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigV2 {
pub owner: Addr,
pub fee: Decimal,
pub recipient: Addr,
}
pub const CONFIG_V1: Item<ConfigV1> = Item::new("config");
pub const CONFIG_V2: Item<ConfigV2> = Item::new("config");
/// 从 V1 迁移到 V2
pub fn migrate_config(storage: &mut dyn Storage, recipient: &Addr) -> StdResult<()> {
let v1 = CONFIG_V1.load(storage)?;
let v2 = ConfigV2 {
owner: v1.owner,
fee: Decimal::percent(v1.fee), // u64 -> Decimal
recipient: recipient.clone(),
};
CONFIG_V2.save(storage, &v2)?;
STORAGE_VERSION.save(storage, &VERSION)?;
Ok(())
}
/// 键迁移:整个 Map 前缀变更
pub const OLD_BALANCES: Map<&Addr, Uint128> = Map::new("bal");
pub const NEW_BALANCES: Map<&Addr, Uint128> = Map::new("balance_v2");
pub fn migrate_balances(storage: &mut dyn Storage) -> StdResult<()> {
let batch: Vec<(Addr, Uint128)> = OLD_BALANCES
.range(storage, None, None, Order::Ascending)
.map(|r| {
let (k, v) = r?;
let addr = Addr::unchecked(String::from_utf8(k)?);
Ok((addr, v))
})
.collect::<StdResult<Vec<_>>>()?;
for (addr, bal) in &batch {
NEW_BALANCES.save(storage, addr, bal)?;
}
Ok(())
}
3. 授权模式
3.1 Owner-Only 模式
最基本也是最重要的访问控制模式。只有一个账户(或合约)拥有管理权限。
use cosmwasm_std::{Addr, Storage};
use cw_storage_plus::Item;
use thiserror::Error;
pub const OWNER: Item<Addr> = Item::new("owner");
#[derive(Error, Debug, PartialEq)]
pub enum AuthError {
#[error("Unauthorized: sender is not the owner")]
NotOwner,
}
/// 检查调用者是否为所有者
pub fn assert_owner(storage: &dyn Storage, sender: &Addr) -> Result<(), AuthError> {
let owner = OWNER.load(storage).map_err(|_| AuthError::NotOwner)?;
if sender != &owner {
return Err(AuthError::NotOwner);
}
Ok(())
}
// 在合约入口中使用
pub fn execute_set_fee(
deps: DepsMut,
info: MessageInfo,
fee: Decimal,
) -> Result<Response, ContractError> {
assert_owner(deps.storage, &info.sender).map_err(|_| ContractError::Unauthorized)?;
// ... 执行业务逻辑
Ok(Response::new().add_attribute("action", "set_fee"))
}
3.2 基于角色的访问控制 (RBAC)
RBAC 允许定义多个角色并为地址分配角色。比 Owner-only 更灵活。
use cosmwasm_std::{Addr, Storage};
use cw_storage_plus::Map;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// 角色枚举
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum Role {
Admin,
Minter,
Burner,
Pauser,
Operator,
}
/// 角色成员存储: Role -> Vec<Addr>
pub const ROLE_MEMBERS: Map<&Role, Vec<Addr>> = Map::new("roles");
impl Role {
pub fn as_str(&self) -> &'static str {
match self {
Role::Admin => "admin",
Role::Minter => "minter",
Role::Burner => "burner",
Role::Pauser => "pauser",
Role::Operator => "operator",
}
}
}
/// 授权管理器
pub struct RBAC;
impl RBAC {
pub fn has_role(storage: &dyn Storage, addr: &Addr, role: &Role) -> bool {
ROLE_MEMBERS
.may_load(storage, role)
.unwrap_or_default()
.contains(addr)
}
pub fn add_role(
storage: &mut dyn Storage,
admin: &Addr,
addr: &Addr,
role: &Role,
) -> StdResult<()> {
// 仅 Admin 可以管理角色
if !Self::has_role(storage, admin, &Role::Admin) {
return Err(StdError::generic_err("Unauthorized: not admin"));
}
ROLE_MEMBERS.update(storage, role, |existing| -> StdResult<Vec<Addr>> {
let mut members = existing.unwrap_or_default();
if !members.contains(addr) {
members.push(addr.clone());
}
Ok(members)
})?;
Ok(())
}
pub fn remove_role(
storage: &mut dyn Storage,
admin: &Addr,
addr: &Addr,
role: &Role,
) -> StdResult<()> {
if !Self::has_role(storage, admin, &Role::Admin) {
return Err(StdError::generic_err("Unauthorized: not admin"));
}
ROLE_MEMBERS.update(storage, role, |existing| -> StdResult<Vec<Addr>> {
let mut members = existing.unwrap_or_default();
members.retain(|m| m != addr);
Ok(members)
})?;
Ok(())
}
pub fn assert_role(
storage: &dyn Storage,
addr: &Addr,
role: &Role,
) -> Result<(), ContractError> {
if !Self::has_role(storage, addr, role) {
return Err(ContractError::Unauthorized);
}
Ok(())
}
}
/// 在合约中使用
pub fn execute_mint(
deps: DepsMut,
info: MessageInfo,
to: Addr,
amount: Uint128,
) -> Result<Response, ContractError> {
RBAC::assert_role(deps.storage, &info.sender, &Role::Minter)?;
BALANCES.update(deps.storage, &to, |bal| -> StdResult<Uint128> {
Ok(bal.unwrap_or_default() + amount)
})?;
Ok(Response::new()
.add_attribute("action", "mint")
.add_attribute("to", to)
.add_attribute("amount", amount))
}
3.3 Allowance 模式 (CW1)
CW1 (Subkeys) 标准允许代币持有者授权给子密钥有限的代币使用额度。这是 ERC20 Approve/TransferFrom 的泛化版本。
use cosmwasm_std::{Addr, Coin, Storage};
use cw_storage_plus::Map;
use cw_utils::Expiration;
/// 授权: (授权者, 被授权者) -> 授权详情
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Allowance {
pub amount: Coin,
pub expires: Expiration,
}
pub const ALLOWANCES: Map<(&Addr, &Addr), Allowance> = Map::new("allow");
/// 增加授权
pub fn increase_allowance(
storage: &mut dyn Storage,
owner: &Addr,
spender: &Addr,
amount: Coin,
expires: Expiration,
) -> StdResult<Allowance> {
ALLOWANCES.update(storage, (owner, spender), |existing| -> StdResult<Allowance> {
let mut allow = existing.unwrap_or(Allowance {
amount: Coin::new(0u128, &amount.denom),
expires: Expiration::Never {},
});
allow.amount.amount += amount.amount;
if expires > &allow.expires {
allow.expires = expires;
}
Ok(allow)
})
}
/// 消费授权
pub fn spend_allowance(
storage: &mut dyn Storage,
owner: &Addr,
spender: &Addr,
amount: Coin,
block: &BlockInfo,
) -> StdResult<()> {
let mut allow = ALLOWANCES.load(storage, (owner, spender))?;
// 检查过期
if allow.expires.is_expired(block) {
return Err(StdError::generic_err("Allowance expired"));
}
// 检查额度
if allow.amount.amount < amount.amount {
return Err(StdError::generic_err("Insufficient allowance"));
}
allow.amount.amount -= amount.amount;
if allow.amount.amount.is_zero() {
ALLOWANCES.remove(storage, (owner, spender));
} else {
ALLOWANCES.save(storage, (owner, spender), &allow)?;
}
Ok(())
}
/// CW1 完整实现示例
pub fn execute_send_from(
deps: DepsMut,
info: MessageInfo,
from: Addr,
to: Addr,
amount: Coin,
) -> Result<Response, ContractError> {
// 检查授权
let allow = ALLOWANCES.load(deps.storage, (&from, &info.sender))?;
if allow.amount.amount < amount.amount {
return Err(ContractError::InsufficientAllowance {});
}
if allow.expires.is_expired(&deps.block) {
return Err(ContractError::AllowanceExpired {});
}
// 消耗授权
let mut updated = allow.clone();
updated.amount.amount -= amount.amount;
if updated.amount.amount.is_zero() {
ALLOWANCES.remove(deps.storage, (&from, &info.sender));
} else {
ALLOWANCES.save(deps.storage, (&from, &info.sender), &updated)?;
}
// 执行转账
BALANCES.update(deps.storage, &from, |b| -> StdResult<Uint128> {
Ok(b.unwrap_or_default().checked_sub(amount.amount)?)
})?;
BALANCES.update(deps.storage, &to, |b| -> StdResult<Uint128> {
Ok(b.unwrap_or_default() + amount.amount)
})?;
Ok(Response::new()
.add_attribute("action", "send_from")
.add_attribute("from", from)
.add_attribute("to", to)
.add_attribute("amount", amount.amount))
}
3.4 多签模式 (CW3)
CW3 定义了一个多签名投票合约接口,常用于 DAO 治理、金库管理。
use cosmswasm_std::{Addr, Storage, Uint128};
use cw_storage_plus::{Item, Map};
/// 投票配置
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Voter {
pub weight: Uint128,
pub voted: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MultisigConfig {
pub threshold: Uint128,
pub total_weight: Uint128,
pub voters: Vec<Addr>,
}
/// 提案
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Proposal {
pub id: u64,
pub title: String,
pub description: String,
pub msgs: Vec<CosmosMsg>,
pub status: ProposalStatus,
pub yes_weight: Uint128,
pub no_weight: Uint128,
pub expires: Expiration,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ProposalStatus {
Pending,
Passed,
Rejected,
Executed,
}
pub const CONFIG: Item<MultisigConfig> = Item::new("config");
pub const PROPOSALS: Map<u64, Proposal> = Map::new("prop");
pub const PROPOSAL_COUNT: Item<u64> = Item::new("prop_count");
pub const VOTES: Map<(u64, &Addr), VoteResponse> = Map::new("vote");
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VoteResponse {
pub voter: Addr,
pub vote: Vote,
pub weight: Uint128,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum Vote {
Yes,
No,
}
/// 创建提案
pub fn execute_create_proposal(
deps: DepsMut,
info: MessageInfo,
title: String,
description: String,
msgs: Vec<CosmosMsg>,
expires: Expiration,
) -> Result<Response, ContractError> {
let cfg = CONFIG.load(deps.storage)?;
// 检查调用者是否为投票人
if !cfg.voters.contains(&info.sender) {
return Err(ContractError::Unauthorized {});
}
let id = PROPOSAL_COUNT.update(deps.storage, |c| -> StdResult<u64> {
Ok(c + 1)
})?;
let proposal = Proposal {
id,
title,
description,
msgs,
status: ProposalStatus::Pending,
yes_weight: Uint128::zero(),
no_weight: Uint128::zero(),
expires,
};
PROPOSALS.save(deps.storage, id, &proposal)?;
Ok(Response::new()
.add_attribute("action", "create_proposal")
.add_attribute("proposal_id", id.to_string()))
}
/// 投票
pub fn execute_vote(
deps: DepsMut,
info: MessageInfo,
proposal_id: u64,
vote: Vote,
) -> Result<Response, ContractError> {
let cfg = CONFIG.load(deps.storage)?;
// 检查投票权
let voter_weight = cfg
.voters
.iter()
.position(|v| v == &info.sender)
.map(|_| Uint128::new(1)) // 简化为等权重
.ok_or(ContractError::Unauthorized {})?;
let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?;
// 检查是否已过期
if proposal.expires.is_expired(&deps.block) {
return Err(ContractError::ProposalExpired {});
}
// 检查是否已投票
if VOTES.has(deps.storage, (proposal_id, &info.sender)) {
return Err(ContractError::AlreadyVoted {});
}
// 记录投票
VOTES.save(
deps.storage,
(proposal_id, &info.sender),
&VoteResponse {
voter: info.sender.clone(),
weight: voter_weight,
vote: vote.clone(),
},
)?;
// 更新计票
match vote {
Vote::Yes => proposal.yes_weight += voter_weight,
Vote::No => proposal.no_weight += voter_weight,
}
// 检查是否达到阈值
if proposal.yes_weight >= cfg.threshold {
proposal.status = ProposalStatus::Passed;
} else if proposal.no_weight > Uint128::zero()
&& cfg.total_weight - proposal.no_weight < cfg.threshold
{
proposal.status = ProposalStatus::Rejected;
}
PROPOSALS.save(deps.storage, proposal_id, &proposal)?;
Ok(Response::new()
.add_attribute("action", "vote")
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("vote", format!("{:?}", vote)))
}
/// 执行提案
pub fn execute_execute(
deps: DepsMut,
info: MessageInfo,
proposal_id: u64,
) -> Result<Response, ContractError> {
let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?;
if proposal.status != ProposalStatus::Passed {
return Err(ContractError::ProposalNotPassed {});
}
proposal.status = ProposalStatus::Executed;
PROPOSALS.save(deps.storage, proposal_id, &proposal)?;
Ok(Response::new()
.add_messages(proposal.msgs)
.add_attribute("action", "execute")
.add_attribute("proposal_id", proposal_id.to_string()))
}
4. 工厂模式
4.1 Code ID + Instantiate2 确定性地址
Instantiate2 允许根据 (deployer, salt, code_id) 计算出确定性合约地址,无需等待交易确认即可预知地址。
use cosmwasm_std::{
instantiate2_address, Coin, CodeInfoResponse, DepsMut, Env, HexBinary, MessageInfo,
Response, WasmMsg,
};
/// 计算确定性地址
pub fn predict_contract_address(
deps: &DepsMut,
env: &Env,
code_id: u64,
salt: &[u8],
msg: &Binary,
) -> StdResult<Addr> {
let code_info: CodeInfoResponse = deps.querier.query_wasm_code_info(code_id)?;
let canonical_addr = deps.api.addr_canonicalize(env.contract.address.as_str())?;
let canonical = instantiate2_address(
code_info.checksum.as_slice(),
&canonical_addr,
salt,
)?;
let addr = deps.api.addr_humanize(&canonical)?;
Ok(addr)
}
/// 工厂合约创建用户合约
pub fn execute_create_vault(
deps: DepsMut,
env: Env,
info: MessageInfo,
salt: HexBinary,
owner: Addr,
) -> Result<Response, ContractError> {
// 读取工厂配置
let cfg = FACTORY_CONFIG.load(deps.storage)?;
// 计算预计地址
let init_msg = VaultInstantiateMsg { owner };
let predicted = predict_contract_address(
&deps,
&env,
cfg.vault_code_id,
&salt,
&to_binary(&init_msg)?,
)?;
// 检查地址是否已被占用
let existing = deps
.querier
.query_wasm_contract_info(predicted.clone());
if existing.is_ok() {
return Err(ContractError::ContractAlreadyExists(predicted));
}
// 使用 Instantiate2 部署
let msg = WasmMsg::Instantiate2 {
admin: Some(env.contract.address.to_string()),
code_id: cfg.vault_code_id,
msg: to_binary(&init_msg)?,
funds: info.funds,
label: format!("vault_{}", salt.to_hex()),
salt: salt.into(),
};
Ok(Response::new()
.add_message(msg)
.add_attribute("action", "create_vault")
.add_attribute("vault", predicted))
}
4.2 Reply-based 工厂 (SubMsg)
Reply 模式让工厂合约可以在子合约实例化后获得回复,执行后续逻辑(如初始化状态)。
use cosmwasm_std::{
Coin, Reply, Response, StdError, StdResult, SubMsg, SubMsgResponse, SubMsgResult,
WasmMsg,
};
use cw_utils::parse_reply_instantiate_data;
/// Reply ID 常量
pub const CREATE_VAULT_REPLY_ID: u64 = 1;
pub const CREATE_TOKEN_REPLY_ID: u64 = 2;
/// 创建并初始化保险箱
pub fn execute_create_managed_vault(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let cfg = FACTORY_CONFIG.load(deps.storage)?;
let init_msg = VaultInstantiateMsg {
owner: info.sender.clone(),
};
let sub_msg = SubMsg::reply_on_success(
WasmMsg::Instantiate {
admin: Some(env.contract.address.to_string()),
code_id: cfg.vault_code_id,
msg: to_binary(&init_msg)?,
funds: info.funds,
label: format!("managed_vault_{}", info.sender),
},
CREATE_VAULT_REPLY_ID,
);
Ok(Response::new()
.add_submessage(sub_msg)
.add_attribute("action", "create_managed_vault")
.add_attribute("creator", info.sender))
}
/// Reply 处理器
pub fn reply_create_vault(
deps: DepsMut,
env: Env,
reply: Reply,
) -> Result<Response, ContractError> {
match reply.result {
SubMsgResult::Ok(response) => {
// 从回复中解析新创建的合约地址
let data = parse_reply_instantiate_data(response)?;
let vault_addr = deps.api.addr_validate(&data.contract_address)?;
// 记录工厂管理的保险箱
MANAGED_VAULTS.save(
deps.storage,
&vault_addr,
&ManagedVault {
address: vault_addr.clone(),
created_at: env.block.height,
creator: env.contract.address.clone(),
},
)?;
// 可选的后续初始化
let init_msg = VaultConfigureMsg {
factory: env.contract.address.to_string(),
};
let exec_msg = WasmMsg::Execute {
contract_addr: vault_addr.to_string(),
msg: to_binary(&init_msg)?,
funds: vec![],
};
Ok(Response::new()
.add_message(exec_msg)
.add_attribute("action", "vault_created")
.add_attribute("vault", vault_addr))
}
SubMsgResult::Err(err) => {
// 创建失败处理
Err(ContractError::CreationFailed(err))
}
}
}
/// 将 Reply 注册到合约入口
pub fn reply(deps: DepsMut, env: Env, reply: Reply) -> Result<Response, ContractError> {
match reply.id {
CREATE_VAULT_REPLY_ID => reply_create_vault(deps, env, reply),
CREATE_TOKEN_REPLY_ID => reply_create_token(deps, env, reply),
_ => Err(ContractError::UnknownReplyId(reply.id)),
}
}
4.3 工厂带创建费模式
在某些场景下,工厂合约需要收取创建费用。
use cosmwasm_std::{coin, BankMsg, Coin, Decimal, Uint128};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct FactoryConfig {
pub vault_code_id: u64,
pub creation_fee: Coin,
pub fee_collector: Addr,
pub protocol_fee_percent: Decimal,
}
pub const FACTORY_CONFIG: Item<FactoryConfig> = Item::new("fcfg");
/// 收取创建费
pub fn assert_creation_fee(
funds: &[Coin],
config: &FactoryConfig,
) -> Result<(), ContractError> {
let fee_amount = funds
.iter()
.find(|c| c.denom == config.creation_fee.denom)
.map(|c| c.amount)
.unwrap_or(Uint128::zero());
if fee_amount < config.creation_fee.amount {
return Err(ContractError::InsufficientFee {
required: config.creation_fee.clone(),
provided: fee_amount,
});
}
Ok(())
}
/// 创建带分级费用的保险箱
pub fn execute_create_tiered_vault(
deps: DepsMut,
env: Env,
info: MessageInfo,
tier: VaultTier,
) -> Result<Response, ContractError> {
let cfg = FACTORY_CONFIG.load(deps.storage)?;
// 根据层级计算费用
let tier_fee = match tier {
VaultTier::Basic => coin(100_000_000, "umsg"), // 100 MSG
VaultTier::Pro => coin(1_000_000_000, "umsg"), // 1,000 MSG
VaultTier::Enterprise => coin(10_000_000_000, "umsg"), // 10,000 MSG
};
// 验证费用
let paid = info
.funds
.iter()
.find(|c| c.denom == "umsg")
.map(|c| c.amount)
.unwrap_or(Uint128::zero());
if paid < tier_fee.amount {
return Err(ContractError::InsufficientFee {
required: tier_fee,
provided: paid,
});
}
// 协议抽成
let protocol_fee = tier_fee
.amount
.multiply_ratio(cfg.protocol_fee_percent.numerator(), Uint128::new(100));
let collector_fee = tier_fee.amount - protocol_fee;
let mut msgs: Vec<CosmosMsg> = vec![];
// 分配费用
if !collector_fee.is_zero() {
msgs.push(BankMsg::Send {
to_address: cfg.fee_collector.to_string(),
amount: vec![coin(collector_fee.u128(), "umsg")],
}
.into());
}
// 创建保险箱
let init_msg = VaultInstantiateMsg {
owner: info.sender,
tier,
};
let sub_msg = SubMsg::reply_on_success(
WasmMsg::Instantiate {
admin: Some(env.contract.address.to_string()),
code_id: cfg.vault_code_id,
msg: to_binary(&init_msg)?,
funds: vec![coin(protocol_fee.u128(), "umsg")],
label: format!("tiered_vault_{}", env.block.time.nanos()),
},
CREATE_VAULT_REPLY_ID,
);
Ok(Response::new()
.add_messages(msgs)
.add_submessage(sub_msg)
.add_attribute("action", "create_tiered_vault")
.add_attribute("tier", format!("{:?}", tier)))
}
5. 回调模式
5.1 SubMsg 组合模式
SubMsg 是 CosmWasm 实现合约组合最重要的机制。它允许合约发送消息并在消息执行后获得回调。
use cosmwasm_std::{Reply, SubMsg, SubMsgResponse, SubMsgResult, WasmMsg};
/// 原子性批量转账:如果任一转账失败,所有转账回滚
pub fn execute_atomic_batch_transfer(
deps: DepsMut,
info: MessageInfo,
transfers: Vec<(Addr, Coin)>,
) -> Result<Response, ContractError> {
let sender = &info.sender;
let mut sub_msgs: Vec<SubMsg> = vec![];
for (idx, (recipient, amount)) in transfers.iter().enumerate() {
let transfer = BankMsg::Send {
to_address: recipient.to_string(),
amount: vec![amount.clone()],
};
// 使用 reply_on_error: 任一失败则全部回滚
sub_msgs.push(SubMsg::reply_on_error(transfer, idx as u64));
}
Ok(Response::new()
.add_submessages(sub_msgs)
.add_attribute("action", "atomic_batch_transfer")
.add_attribute("count", transfers.len().to_string()))
}
/// 异步不需要回复的消息(fire-and-forget)
pub fn execute_batch_notify(
_deps: DepsMut,
_info: MessageInfo,
notifications: Vec<Addr>,
) -> Result<Response, ContractError> {
let sub_msgs: Vec<SubMsg> = notifications
.into_iter()
.map(|addr| {
let msg = WasmMsg::Execute {
contract_addr: addr.to_string(),
msg: to_binary(&NotifyMsg::Process {}).unwrap(),
funds: vec![],
};
// 不关心结果
SubMsg::new(msg)
})
.collect();
Ok(Response::new()
.add_submessages(sub_msgs)
.add_attribute("action", "batch_notify")
.add_attribute("count", sub_msgs.len().to_string()))
}
5.2 Reply 处理器模式
Reply 处理器是处理 SubMsg 返回结果的标准方式。
use cosmwasm_std::{
from_binary, to_binary, Reply, StdError, SubMsgExecutionResponse, WasmMsg,
};
/// 跨合约 Swap + 提供流动性的组合操作
pub const SWAP_REPLY_ID: u64 = 1;
pub const PROVIDE_LIQUIDITY_REPLY_ID: u64 = 2;
/// 第一次 Reply: 处理 Swap 结果
pub fn reply_swap(deps: DepsMut, env: Env, reply: Reply) -> Result<Response, ContractError> {
match reply.result {
SubMsgResult::Ok(response) => {
// 解析 swap 合约的返回值
let swap_result: SwapResponse = parse_reply_data(response)?;
// 使用 swap 结果构建下一个操作
let provide_msg = WasmMsg::Execute {
contract_addr: env.contract.address.to_string(),
msg: to_binary(&ExecuteMsg::ProvideLiquidity {
token_a: swap_result.token_out,
token_b: swap_result.token_in,
})?,
funds: vec![],
};
// 继续链式调用
let sub = SubMsg::reply_on_success(provide_msg, PROVIDE_LIQUIDITY_REPLY_ID);
Ok(Response::new()
.add_submessage(sub)
.add_attribute("action", "swap_reply")
.add_attribute("swap_amount", swap_result.amount_out))
}
SubMsgResult::Err(err) => {
// Swap 失败处理
Err(ContractError::SwapFailed(err))
}
}
}
/// 通用 Reply 数据解析
pub fn parse_reply_data<T: DeserializeOwned>(
response: SubMsgResponse,
) -> StdResult<T> {
let data = response
.data
.ok_or_else(|| StdError::generic_err("No reply data"))?;
from_binary(&data)
}
/// 聚合多个 Reply 结果
pub struct ReplyAggregator;
impl ReplyAggregator {
pub fn aggregate(replies: &[Reply]) -> StdResult<AggregatedResult> {
let mut successes = 0u64;
let mut failures = 0u64;
let mut data = vec![];
for reply in replies {
match &reply.result {
SubMsgResult::Ok(resp) => {
successes += 1;
if let Some(d) = &resp.data {
data.push(d.clone());
}
}
SubMsgResult::Err(_) => {
failures += 1;
}
}
}
Ok(AggregatedResult {
successes,
failures,
data,
})
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct AggregatedResult {
pub successes: u64,
pub failures: u64,
pub data: Vec<Binary>,
}
5.3 跨合约查询模式
CosmWasm 的查询是同步的,但需要显式通过 QuerierWrapper 调用。
use cosmwasm_std::{QuerierWrapper, QueryRequest, WasmQuery};
/// 代币信息查询器
pub struct TokenQuerier<'a> {
querier: &'a QuerierWrapper<'a>,
}
impl<'a> TokenQuerier<'a> {
pub fn new(querier: &'a QuerierWrapper<'a>) -> Self {
Self { querier }
}
/// 查询 CW20 代币余额
pub fn query_balance(
&self,
token_addr: &Addr,
owner: &Addr,
) -> StdResult<Uint128> {
let query = CW20QueryMsg::Balance {
address: owner.to_string(),
};
let balance: BalanceResponse = self
.querier
.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: token_addr.to_string(),
msg: to_binary(&query)?,
}))?;
Ok(balance.balance)
}
/// 批量查询代币余额(通过多个独立查询)
pub fn batch_query_balances(
&self,
queries: Vec<(Addr, Addr)>,
) -> Vec<StdResult<Uint128>> {
queries
.into_iter()
.map(|(token, owner)| self.query_balance(&token, &owner))
.collect()
}
/// 查询合约的总供应量
pub fn query_total_supply(
&self,
token_addr: &Addr,
) -> StdResult<Uint128> {
let supply: TotalSupplyResponse = self
.querier
.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: token_addr.to_string(),
msg: to_binary(&CW20QueryMsg::TotalSupply {})?,
}))?;
Ok(supply.total)
}
}
/// 链上价格预言机查询
pub fn query_twap_price(
querier: &QuerierWrapper,
oracle_addr: &Addr,
pair: &str,
window: u64,
) -> StdResult<Decimal> {
let query = OracleQueryMsg::TwapPrice {
pair: pair.to_string(),
window_seconds: window,
};
let price: TwapPriceResponse = querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: oracle_addr.to_string(),
msg: to_binary(&query)?,
}))?;
Ok(price.price)
}
/// 跨合约查询聚合
pub fn aggregate_prices(
querier: &QuerierWrapper,
sources: Vec<(Addr, String)>,
) -> StdResult<Vec<(String, Decimal)>> {
let mut prices = vec![];
for (oracle, pair) in sources {
let price = query_twap_price(querier, &oracle, &pair, 3600)?;
prices.push((pair, price));
}
Ok(prices)
}
6. 升级模式
6.1 MigrateMsg 模式
CosmWasm 通过 MigrateMsg 入口支持合约代码升级。这是最基础也最重要的升级模式。
use cosmwasm_std::{
from_binary, to_binary, Binary, DepsMut, Env, MessageInfo, Response, StdError,
};
/// 迁移消息(只能由合约管理员调用)
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MigrateMsg {
/// 新版本号
pub version: String,
/// 迁移特定参数
pub params: Option<Binary>,
}
/// 合约当前版本
pub const CONTRACT_VERSION: Item<String> = Item::new("contract_version");
/// 迁移入口
pub fn migrate(
deps: DepsMut,
_env: Env,
msg: MigrateMsg,
) -> Result<Response, ContractError> {
// 记录迁移前的版本
let old_version = CONTRACT_VERSION
.may_load(deps.storage)?
.unwrap_or_else(|| "0.0.0".to_string());
// 版本校验(防止降级)
if semver_compare(&msg.version, &old_version) == Ordering::Less {
return Err(ContractError::MigrationError(
"Cannot downgrade contract version".to_string(),
));
}
// 按版本顺序执行迁移逻辑
let mut response = Response::new()
.add_attribute("action", "migrate")
.add_attribute("from_version", &old_version)
.add_attribute("to_version", &msg.version);
// 版本特定迁移
match old_version.as_str() {
"0.0.0" | "1.0.0" => {
response = response.add_messages(migrate_v1_to_v2(deps.storage)?);
}
"1.1.0" => {
response = response.add_messages(migrate_v1_1_to_v2(deps.storage)?);
}
_ => {} // 当前版本,无需迁移
}
// 更新版本
CONTRACT_VERSION.save(deps.storage, &msg.version)?;
Ok(response)
}
/// 从 V1 到 V2 的存储迁移
fn migrate_v1_to_v2(storage: &mut dyn Storage) -> StdResult<Vec<CosmosMsg>> {
// 示例: 将旧的 u64 fee 迁移到新的 Decimal fee
let old_config = OLD_CONFIG.load(storage)?;
let new_config = NewConfig {
owner: old_config.owner,
fee: Decimal::percent(old_config.fee as u64),
recipient: old_config.owner.clone(),
};
NEW_CONFIG.save(storage, &new_config)?;
Ok(vec![])
}
pub fn migrate_v1_1_to_v2(storage: &mut dyn Storage) -> StdResult<Vec<CosmosMsg>> {
// 小版本迁移逻辑
Ok(vec![])
}
/// 检测是否为迁移操作
pub fn is_migration(deps: &DepsMut) -> bool {
// 通过检查入口来判断
// 在实际代码中,这通常通过单独的处理逻辑实现
false
}
6.2 存储迁移与版本控制
更复杂的存储迁移需要完整的版本控制方案。
use cosmwasm_std::Storage;
use cw_storage_plus::Item;
/// 存储版本控制
pub const STORAGE_VERSION: Item<u64> = Item::new("sv");
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum StorageState {
Uninitialized,
V1,
V2,
V3,
}
/// 版本化迁移管理器
pub struct StorageMigrator;
impl StorageMigrator {
pub fn current_version(storage: &dyn Storage) -> u64 {
STORAGE_VERSION.may_load(storage).unwrap_or(0)
}
pub fn set_version(storage: &mut dyn Storage, version: u64) -> StdResult<()> {
STORAGE_VERSION.save(storage, &version)
}
/// 执行所有必要的迁移
pub fn perform_migrations(
storage: &mut dyn Storage,
target_version: u64,
) -> StdResult<()> {
let current = Self::current_version(storage);
for version in (current + 1)..=target_version {
Self::apply_migration(storage, version)?;
}
Ok(())
}
fn apply_migration(storage: &mut dyn Storage, version: u64) -> StdResult<()> {
match version {
1 => Self::migrate_to_v1(storage),
2 => Self::migrate_to_v2(storage),
3 => Self::migrate_to_v3(storage),
_ => Err(StdError::generic_err(format!(
"Unknown migration version: {}",
version
))),
}
}
fn migrate_to_v1(storage: &mut dyn Storage) -> StdResult<()> {
// 初始设置
Self::set_version(storage, 1)
}
fn migrate_to_v2(storage: &mut dyn Storage) -> StdResult<()> {
// V1 -> V2: 添加新字段
// 旧配置
let old: OldConfig = Item::new("config").load(storage)?;
// 迁移到新配置
let new = NewConfig {
owner: old.owner,
fee: old.fee,
recipient: old.owner.clone(), // 新字段默认值
};
Item::<NewConfig>::new("config").save(storage, &new)?;
Self::set_version(storage, 2)
}
fn migrate_to_v3(storage: &mut dyn Storage) -> StdResult<()> {
// V2 -> V3: 键前缀迁移
// 将所有以 "old_balances/" 开头的键迁移到 "balances/"
let old_map = Map::<&Addr, Uint128>::new("old_balances");
let new_map = Map::<&Addr, Uint128>::new("balances");
let entries: Vec<(Addr, Uint128)> = old_map
.range(storage, None, None, Order::Ascending)
.map(|r| {
let (k, v) = r?;
Ok((Addr::unchecked(String::from_utf8(k)?), v))
})
.collect::<StdResult<_>>()?;
for (addr, bal) in entries {
new_map.save(storage, &addr, &bal)?;
}
Self::set_version(storage, 3)
}
}
/// 完整的迁移入口
pub fn migrate_v2(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
StorageMigrator::perform_migrations(deps.storage, 3)?;
Ok(Response::new()
.add_attribute("action", "migrate_v2")
.add_attribute("version", "3"))
}
6.3 Proxy 模式
Proxy 模式通过代理合约将调用 delegator 给实现合约,实现逻辑的可升级性。
use cosmwasm_std::{
to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, WasmMsg,
};
/// 代理合约存储
pub const IMPLEMENTATION: Item<Addr> = Item::new("implementation");
pub const ADMIN: Item<Addr> = Item::new("admin");
/// 代理合约: 将执行转发给实现
pub fn execute_proxy(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: Binary,
) -> Result<Response, ContractError> {
// 仅管理员可升级
if info.sender == ADMIN.load(deps.storage)? {
if let Ok(upgrade) = from_binary::<UpgradeMsg>(&msg) {
return handle_upgrade(deps, env, info, upgrade);
}
}
let impl_addr = IMPLEMENTATION.load(deps.storage)?;
// 转发调用到实现合约
let exec_msg = WasmMsg::Execute {
contract_addr: impl_addr.to_string(),
msg,
funds: info.funds,
};
Ok(Response::new()
.add_message(exec_msg)
.add_attribute("action", "proxy_execute"))
}
/// 升级到新实现
pub fn handle_upgrade(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: UpgradeMsg,
) -> Result<Response, ContractError> {
// 权限检查
let admin = ADMIN.load(deps.storage)?;
if info.sender != admin {
return Err(ContractError::Unauthorized {});
}
let new_impl = deps.api.addr_validate(&msg.new_implementation)?;
IMPLEMENTATION.save(deps.storage, &new_impl)?;
// 可选:调用实现合约的初始化函数
if let Some(init_msg) = msg.init_msg {
let wasm = WasmMsg::Execute {
contract_addr: new_impl.to_string(),
msg: to_binary(&init_msg)?,
funds: vec![],
};
return Ok(Response::new()
.add_message(wasm)
.add_attribute("action", "upgrade")
.add_attribute("new_impl", new_impl));
}
Ok(Response::new()
.add_attribute("action", "upgrade")
.add_attribute("new_impl", new_impl))
}
/// 查询转发(将查询委托给实现合约)
pub fn query_proxy(deps: Deps, _env: Env, msg: Binary) -> StdResult<Binary> {
let impl_addr = IMPLEMENTATION.load(deps.storage)?;
// 构造跨合约查询
let query = cosmwasm_std::QueryRequest::Wasm(cosmwasm_std::WasmQuery::Smart {
contract_addr: impl_addr.to_string(),
msg,
});
deps.querier.query(&query)
}
7. 经济模式
7.1 托管 (Escrow) 模式
托管模式实现了一个中立的第三方保管资金,在条件满足时释放。
use cosmwasm_std::{
coins, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, StdResult, Uint128,
};
use cw_storage_plus::{Item, Map};
/// 托管交易
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Escrow {
pub id: u64,
pub sender: Addr,
pub recipient: Addr,
pub amount: Coin,
pub arbiter: Addr,
pub status: EscrowStatus,
pub expires: Expiration,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum EscrowStatus {
Pending,
Approved,
Refunded,
Disputed,
}
pub const ESCROW_COUNT: Item<u64> = Item::new("escrow_count");
pub const ESCROWS: Map<u64, Escrow> = Map::new("escrows");
/// 创建托管
pub fn execute_create_escrow(
deps: DepsMut,
env: Env,
info: MessageInfo,
recipient: Addr,
arbiter: Addr,
duration_seconds: u64,
) -> Result<Response, ContractError> {
let funds = info.funds;
if funds.len() != 1 || funds[0].amount.is_zero() {
return Err(ContractError::InvalidFunds {});
}
let id = ESCROW_COUNT.update(deps.storage, |c| Ok(c + 1))?;
let escrow = Escrow {
id,
sender: info.sender.clone(),
recipient,
amount: funds[0].clone(),
arbiter,
status: EscrowStatus::Pending,
expires: Expiration::AtTime(env.block.time.plus_seconds(duration_seconds)),
};
ESCROWS.save(deps.storage, id, &escrow)?;
Ok(Response::new()
.add_attribute("action", "create_escrow")
.add_attribute("id", id.to_string())
.add_attribute("amount", escrow.amount.to_string()))
}
/// 批准释放资金
pub fn execute_approve(
deps: DepsMut,
_env: Env,
info: MessageInfo,
id: u64,
) -> Result<Response, ContractError> {
let mut escrow = ESCROWS.load(deps.storage, id)?;
// 仅仲裁人可以批准
if info.sender != escrow.arbiter {
return Err(ContractError::Unauthorized {});
}
if escrow.status != EscrowStatus::Pending {
return Err(ContractError::InvalidStatus {});
}
escrow.status = EscrowStatus::Approved;
ESCROWS.save(deps.storage, id, &escrow)?;
// 转账给接收方
let transfer = BankMsg::Send {
to_address: escrow.recipient.to_string(),
amount: vec![escrow.amount.clone()],
};
Ok(Response::new()
.add_message(transfer)
.add_attribute("action", "approve")
.add_attribute("id", id.to_string()))
}
/// 退款
pub fn execute_refund(
deps: DepsMut,
env: Env,
info: MessageInfo,
id: u64,
) -> Result<Response, ContractError> {
let mut escrow = ESCROWS.load(deps.storage, id)?;
// 发起方可以在过期后退款
if info.sender == escrow.sender {
if !escrow.expires.is_expired(&env.block) {
return Err(ContractError::NotExpired {});
}
} else if info.sender != escrow.arbiter {
return Err(ContractError::Unauthorized {});
}
escrow.status = EscrowStatus::Refunded;
ESCROWS.save(deps.storage, id, &escrow)?;
let refund = BankMsg::Send {
to_address: escrow.sender.to_string(),
amount: vec![escrow.amount.clone()],
};
Ok(Response::new()
.add_message(refund)
.add_attribute("action", "refund")
.add_attribute("id", id.to_string()))
}
7.2 转账抽税 (Fee-on-Transfer) 模式
每次转账时自动收取协议费用。
use cosmwasm_std::{
coin, BankMsg, Coin, Decimal, DepsMut, Env, MessageInfo, Response, StdResult, Uint128,
};
/// 转账抽税配置
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct FeeConfig {
pub fee_rate: Decimal, // 例如 0.003 = 0.3%
pub min_fee: Uint128, // 最小手续费
pub max_fee: Option<Uint128>, // 最大手续费(可选)
pub fee_collector: Addr,
pub denom: String,
}
pub const FEE_CONFIG: Item<FeeConfig> = Item::new("fee_cfg");
/// 计算转账手续费
pub fn calculate_fee(amount: Uint128, config: &FeeConfig) -> Uint128 {
let fee = amount * config.fee_rate;
let fee = fee.max(config.min_fee);
if let Some(max_fee) = config.max_fee {
fee.min(max_fee)
} else {
fee
}
}
/// 带手续费的转账
pub fn execute_transfer_with_fee(
deps: DepsMut,
_env: Env,
info: MessageInfo,
to: Addr,
amount: Uint128,
) -> Result<Response, ContractError> {
let cfg = FEE_CONFIG.load(deps.storage)?;
let sender = &info.sender;
// 计算费用
let fee = calculate_fee(amount, &cfg);
let net_amount = amount.checked_sub(fee)?;
// 检查余额
let sender_balance = BALANCES
.load(deps.storage, sender)?
.checked_sub(amount)?;
// 扣除发送方余额
BALANCES.save(deps.storage, sender, &sender_balance)?;
// 增加接收方余额
let recipient_balance = BALANCES
.load(deps.storage, &to)?
.checked_add(net_amount)?;
BALANCES.save(deps.storage, &to, &recipient_balance)?;
// 收取手续费
let collector_balance = BALANCES
.load(deps.storage, &cfg.fee_collector)?
.checked_add(fee)?;
BALANCES.save(deps.storage, &cfg.fee_collector, &collector_balance)?;
Ok(Response::new()
.add_attribute("action", "transfer_with_fee")
.add_attribute("from", sender)
.add_attribute("to", to)
.add_attribute("amount", net_amount)
.add_attribute("fee", fee))
}
/// 转账时自动抽税(原生代币)
pub fn execute_native_transfer_with_fee(
deps: DepsMut,
env: Env,
info: MessageInfo,
to: String,
amount: Coin,
) -> Result<Response, ContractError> {
let cfg = FEE_CONFIG.load(deps.storage)?;
if amount.denom != cfg.denom {
return Err(ContractError::InvalidDenom {
expected: cfg.denom.clone(),
got: amount.denom,
});
}
let fee = calculate_fee(amount.amount, &cfg);
let net = amount.amount - fee;
let mut msgs = vec![];
// 净额给接收方
msgs.push(BankMsg::Send {
to_address: to,
amount: vec![coin(net.u128(), &cfg.denom)],
}
.into());
// 手续费给收集者
if !fee.is_zero() {
msgs.push(BankMsg::Send {
to_address: cfg.fee_collector.to_string(),
amount: vec![coin(fee.u128(), &cfg.denom)],
}
.into());
}
Ok(Response::new()
.add_messages(msgs)
.add_attribute("action", "native_transfer_with_fee")
.add_attribute("net", net)
.add_attribute("fee", fee))
}
7.3 时间加权平均价格 (TWAP)
TWAP 是 DeFi 领域的核心预言机模式,用于抵抗价格操纵。
use cosmwasm_std::{Decimal, StdResult, Storage, Timestamp, Uint128};
use cw_storage_plus::{Item, Map};
/// TWAP 累积器
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TwapAccumulator {
/// 价格累积量 (价格 * 时间)
pub price_accumulator: Uint128,
/// 最后一次更新时间
pub last_update: Timestamp,
/// 累积交易量
pub volume_accumulator: Uint128,
}
pub const ACCUMULATORS: Map<&str, TwapAccumulator> = Map::new("twap_acc");
pub const TWAP_CONFIG: Item<TwapConfig> = Item::new("twap_cfg");
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct TwapConfig {
pub min_window: u64, // 最小时间窗口(秒)
pub max_window: u64, // 最大时间窗口(秒)
}
/// 更新价格累积器(每次交易时调用)
pub fn update_accumulator(
storage: &mut dyn Storage,
pair: &str,
price: Decimal,
block_time: Timestamp,
) -> StdResult<()> {
let mut acc = ACCUMULATORS
.may_load(storage, pair)?
.unwrap_or(TwapAccumulator {
price_accumulator: Uint128::zero(),
last_update: block_time,
volume_accumulator: Uint128::zero(),
});
let time_elapsed = block_time.seconds() - acc.last_update.seconds();
if time_elapsed > 0 {
// 累积价格 = 价格 * 时间
let accum = price * Uint128::from(time_elapsed);
acc.price_accumulator += accum.atomics();
}
acc.last_update = block_time;
ACCUMULATORS.save(storage, pair, &acc)?;
Ok(())
}
/// 查询 TWAP 价格
pub fn query_twap(
storage: &dyn Storage,
pair: &str,
window_seconds: u64,
current_time: Timestamp,
) -> StdResult<Option<Decimal>> {
let acc = ACCUMULATORS
.may_load(storage, pair)?
.ok_or_else(|| StdError::generic_err("No data for pair"))?;
if acc.last_update == current_time {
return Ok(None); // 无新数据
}
let elapsed = current_time.seconds() - acc.last_update.seconds();
if elapsed < window_seconds {
return Ok(None); // 窗口太小
}
if acc.price_accumulator.is_zero() || elapsed == 0 {
return Ok(None);
}
let twap = Decimal::from_ratio(acc.price_accumulator, elapsed);
Ok(Some(twap))
}
7.4 每秒奖励累积模式
用于 Staking / Farming 场景,高效计算用户应得奖励。
use cosmwasm_std::{Decimal, StdResult, Storage, Timestamp, Uint128};
use cw_storage_plus::{Item, Map};
/// 全局奖励状态
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct GlobalRewardState {
/// 每份额累积奖励
pub accumulated_reward_per_share: Decimal,
/// 最后更新时间
pub last_update: Timestamp,
/// 奖励速率(每秒发放量)
pub reward_rate_per_second: Uint128,
}
/// 用户奖励信息
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct UserReward {
/// 用户质押的份额
pub stake: Uint128,
/// 用户上次更新的累积奖励快照
pub reward_debt: Decimal,
}
pub const GLOBAL_REWARD: Item<GlobalRewardState> = Item::new("global_rwd");
pub const USER_REWARDS: Map<&Addr, UserReward> = Map::new("user_rwds");
pub const TOTAL_STAKE: Item<Uint128> = Item::new("total_stake");
/// 更新全局奖励状态
pub fn update_global_reward(
storage: &mut dyn Storage,
current_time: Timestamp,
) -> StdResult<GlobalRewardState> {
let mut global = GLOBAL_REWARD
.may_load(storage)?
.unwrap_or(GlobalRewardState {
accumulated_reward_per_share: Decimal::zero(),
last_update: current_time,
reward_rate_per_second: Uint128::zero(),
});
let elapsed = current_time.seconds() - global.last_update.seconds();
if elapsed > 0 {
let total = TOTAL_STAKE
.may_load(storage)?
.unwrap_or_default();
if !total.is_zero() {
let reward = global.reward_rate_per_second * Uint128::from(elapsed);
let per_share = Decimal::from_ratio(reward, total);
global.accumulated_reward_per_share += per_share;
}
global.last_update = current_time;
GLOBAL_REWARD.save(storage, &global)?;
}
Ok(global)
}
/// 领取奖励前更新用户状态
pub fn update_user_reward(
storage: &mut dyn Storage,
user: &Addr,
global: &GlobalRewardState,
) -> StdResult<()> {
let mut user_state = USER_REWARDS
.may_load(storage, user)?
.unwrap_or(UserReward {
stake: Uint128::zero(),
reward_debt: Decimal::zero(),
});
user_state.reward_debt =
global.accumulated_reward_per_share * user_state.stake;
USER_REWARDS.save(storage, user, &user_state)?;
Ok(())
}
/// 计算用户待领取奖励
pub fn calculate_pending_reward(
storage: &dyn Storage,
user: &Addr,
) -> StdResult<Uint128> {
let user_state = USER_REWARDS.load(storage, user)?;
let global = GLOBAL_REWARD.load(storage)?;
let pending = global.accumulated_reward_per_share * user_state.stake
- user_state.reward_debt;
Ok(pending.to_uint_floor())
}
/// 存入质押
pub fn execute_stake(
deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Uint128,
) -> Result<Response, ContractError> {
if amount.is_zero() {
return Err(ContractError::ZeroAmount {});
}
// 先更新全局状态
let global = update_global_reward(deps.storage, env.block.time)?;
// 更新用户奖励债务
update_user_reward(deps.storage, &info.sender, &global)?;
// 增加用户质押
USER_REWARDS.update(deps.storage, &info.sender, |existing| {
let mut user = existing.unwrap_or(UserReward {
stake: Uint128::zero(),
reward_debt: Decimal::zero(),
});
user.stake += amount;
user.reward_debt = global.accumulated_reward_per_share * user.stake;
Ok(user)
})?;
// 增加总质押
TOTAL_STAKE.update(deps.storage, |total| Ok(total + amount))?;
// 收取用户代币
let transfer = BankMsg::Send {
to_address: env.contract.address.to_string(),
amount: vec![coin(amount.u128(), "umsg")],
};
Ok(Response::new()
.add_message(transfer)
.add_attribute("action", "stake")
.add_attribute("amount", amount))
}
/// 领取奖励
pub fn execute_claim_reward(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let global = update_global_reward(deps.storage, env.block.time)?;
let user = info.sender;
let user_state = USER_REWARDS.load(deps.storage, &user)?;
let pending = global.accumulated_reward_per_share * user_state.stake
- user_state.reward_debt;
if pending.is_zero() {
return Err(ContractError::NoPendingReward {});
}
// 更新债务
USER_REWARDS.save(
deps.storage,
&user,
&UserReward {
stake: user_state.stake,
reward_debt: global.accumulated_reward_per_share * user_state.stake,
},
)?;
// 发送奖励
let reward_msg = BankMsg::Send {
to_address: user.to_string(),
amount: vec![coin(pending.u128(), "umsg")],
};
Ok(Response::new()
.add_message(reward_msg)
.add_attribute("action", "claim_reward")
.add_attribute("amount", pending.to_string()))
}
8. 安全模式
8.1 Checks-Effects-Interactions (CEI)
CEI 是智能合约安全的基本原则——先检查条件、再更新状态、最后与外部交互。
use cosmwasm_std::{BankMsg, Coin, DepsMut, Env, MessageInfo, Response, StdResult};
/// 安全提现:遵循 CEI 模式
pub fn execute_safe_withdraw(
deps: DepsMut,
_env: Env,
info: MessageInfo,
amount: Coin,
) -> Result<Response, ContractError> {
// ─── 1. CHECKS ───────────────────────────────
// 验证金额有效
if amount.amount.is_zero() {
return Err(ContractError::ZeroAmount {});
}
// 验证用户余额充足
let balance = BALANCES
.load(deps.storage, &info.sender)?;
if balance < amount.amount {
return Err(ContractError::InsufficientBalance {
balance,
required: amount.amount,
});
}
// ─── 2. EFFECTS ──────────────────────────────
// 先更新状态(标记提现)
let new_balance = balance.checked_sub(amount.amount)?;
BALANCES.save(deps.storage, &info.sender, &new_balance)?;
// ─── 3. INTERACTIONS ─────────────────────────
// 最后才发送资金(外部调用)
let transfer = BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![amount],
};
Ok(Response::new()
.add_message(transfer)
.add_attribute("action", "safe_withdraw")
.add_attribute("remaining", new_balance))
}
/// ⚠️ 不安全的模式:先交互后更新状态
/// 如果在 send 之后发生重入,状态已经不一致
pub fn execute_unsafe_withdraw(
deps: DepsMut,
_env: Env,
info: MessageInfo,
amount: Coin,
) -> Result<Response, ContractError> {
// 先交互(不安全!)
let transfer = BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![amount.clone()],
};
// 后更新状态
let balance = BALANCES.load(deps.storage, &info.sender)?;
BALANCES.save(deps.storage, &info.sender, &(balance - amount.amount))?;
// ⚠️ 如果 transfer 触发接收方重入回调,
// 接收方可以在状态更新前多次调用 withdraw
Ok(Response::new()
.add_message(transfer)
.add_attribute("action", "unsafe_withdraw"))
}
8.2 两步所有权转移
将所有权转移分成两步,防止误操作导致合约失控。
use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response, StdResult};
use cw_storage_plus::Item;
/// 当前所有者
pub const OWNER: Item<Addr> = Item::new("owner");
/// 待定的新所有者(两步转移用)
pub const PENDING_OWNER: Item<Option<Addr>> = Item::new("pending_owner");
/// 第一步:发起所有权转移
pub fn execute_transfer_ownership(
deps: DepsMut,
_env: Env,
info: MessageInfo,
new_owner: Addr,
) -> Result<Response, ContractError> {
// 仅当前所有者可调用
let owner = OWNER.load(deps.storage)?;
if info.sender != owner {
return Err(ContractError::Unauthorized {});
}
// 不能将自己设置为待定
if new_owner == owner {
return Err(ContractError::SameOwner {});
}
// 存储待定所有者
PENDING_OWNER.save(deps.storage, &Some(new_owner.clone()))?;
Ok(Response::new()
.add_attribute("action", "transfer_ownership")
.add_attribute("pending_owner", new_owner))
}
/// 第二步:接受所有权
pub fn execute_accept_ownership(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let pending = PENDING_OWNER.load(deps.storage)?;
// 必须是被指定的新所有者
match pending {
Some(addr) if addr == info.sender => {
// 转移所有权
let old = OWNER.load(deps.storage)?;
OWNER.save(deps.storage, &info.sender)?;
PENDING_OWNER.save(deps.storage, &None)?;
Ok(Response::new()
.add_attribute("action", "accept_ownership")
.add_attribute("old_owner", old)
.add_attribute("new_owner", info.sender))
}
_ => Err(ContractError::Unauthorized {}),
}
}
/// 取消所有权转移(紧急情况)
pub fn execute_cancel_ownership(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let owner = OWNER.load(deps.storage)?;
if info.sender != owner {
return Err(ContractError::Unauthorized {});
}
PENDING_OWNER.save(deps.storage, &None)?;
Ok(Response::new()
.add_attribute("action", "cancel_ownership_transfer"))
}
8.3 紧急暂停断路器
在发现安全问题或异常行为时,暂停合约的关键功能。
use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, StdResult};
use cw_storage_plus::Item;
/// 暂停状态
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PauseInfo {
pub paused: bool,
pub paused_by: Option<Addr>,
pub paused_at: Option<u64>,
pub reason: Option<String>,
}
pub const PAUSE_INFO: Item<PauseInfo> = Item::new("pause_info");
pub const PAUSER: Item<Addr> = Item::new("pauser");
/// 暂停合约
pub fn execute_pause(
deps: DepsMut,
env: Env,
info: MessageInfo,
reason: Option<String>,
) -> Result<Response, ContractError> {
// 权限检查
let pauser = PAUSER.load(deps.storage)?;
if info.sender != pauser {
return Err(ContractError::Unauthorized {});
}
let info = PauseInfo {
paused: true,
paused_by: Some(info.sender.clone()),
paused_at: Some(env.block.height),
reason,
};
PAUSE_INFO.save(deps.storage, &info)?;
Ok(Response::new()
.add_attribute("action", "pause")
.add_attribute("paused_at", env.block.height.to_string()))
}
/// 恢复合约
pub fn execute_unpause(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let pauser = PAUSER.load(deps.storage)?;
if info.sender != pauser {
return Err(ContractError::Unauthorized {});
}
PAUSE_INFO.save(
deps.storage,
&PauseInfo {
paused: false,
paused_by: None,
paused_at: None,
reason: None,
},
)?;
Ok(Response::new()
.add_attribute("action", "unpause"))
}
/// 检查是否暂停(作为宏或守卫函数)
pub fn assert_not_paused(storage: &dyn Storage) -> Result<(), ContractError> {
let info = PAUSE_INFO.load(storage)?;
if info.paused {
return Err(ContractError::ContractPaused {
reason: info.reason.unwrap_or_default(),
});
}
Ok(())
}
/// 在入口函数中使用
pub fn execute_deposit(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
// 守卫检查
assert_not_paused(deps.storage)?;
// 正常业务逻辑
Ok(Response::new()
.add_attribute("action", "deposit"))
}
8.4 速率限制
限制单位时间内可操作的金额,防止大额异常提现。
use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response, StdResult, Timestamp, Uint128};
use cw_storage_plus::Map;
/// 速率限制配置
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct RateLimitConfig {
/// 时间窗口(秒)
pub window_seconds: u64,
/// 窗口内最大金额
pub max_amount: Uint128,
/// 每个地址的限额
pub per_address_limit: Option<Uint128>,
}
/// 每个地址的限流状态
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct RateLimitState {
/// 窗口起始时间
pub window_start: Timestamp,
/// 窗口内已用金额
pub used_amount: Uint128,
}
pub const RATE_LIMIT_CONFIG: Item<RateLimitConfig> = Item::new("rlc");
pub const RATE_LIMIT_STATES: Map<&Addr, RateLimitState> = Map::new("rls");
/// 检查并扣减速率额度
pub fn check_and_consume_rate_limit(
storage: &mut dyn Storage,
addr: &Addr,
amount: Uint128,
block_time: Timestamp,
config: &RateLimitConfig,
) -> StdResult<()> {
let mut state = RATE_LIMIT_STATES
.may_load(storage, addr)?
.unwrap_or(RateLimitState {
window_start: block_time,
used_amount: Uint128::zero(),
});
// 如果超出当前窗口,重置
let elapsed = block_time.seconds() - state.window_start.seconds();
if elapsed >= config.window_seconds {
state = RateLimitState {
window_start: block_time,
used_amount: Uint128::zero(),
};
}
// 检查新金额是否会使总额超额
let new_total = state.used_amount + amount;
if new_total > config.max_amount {
return Err(StdError::generic_err(format!(
"Rate limit exceeded: used {} + requested {} > max {}",
state.used_amount, amount, config.max_amount
)));
}
// 检查每个地址的限额
if let Some(per_addr_limit) = config.per_address_limit {
if new_total > per_addr_limit {
return Err(StdError::generic_err(
"Per-address rate limit exceeded",
));
}
}
// 更新状态
state.used_amount = new_total;
RATE_LIMIT_STATES.save(storage, addr, &state)?;
Ok(())
}
/// 全局速率限制(按合约维度)
pub const GLOBAL_RATE_LIMIT: Item<RateLimitState> = Item::new("grl");
pub fn check_global_rate_limit(
storage: &mut dyn Storage,
amount: Uint128,
block_time: Timestamp,
config: &RateLimitConfig,
) -> StdResult<()> {
let mut state = GLOBAL_RATE_LIMIT
.may_load(storage)?
.unwrap_or(RateLimitState {
window_start: block_time,
used_amount: Uint128::zero(),
});
let elapsed = block_time.seconds() - state.window_start.seconds();
if elapsed >= config.window_seconds {
state = RateLimitState {
window_start: block_time,
used_amount: Uint128::zero(),
};
}
if state.used_amount + amount > config.max_amount {
return Err(StdError::generic_err("Global rate limit exceeded"));
}
state.used_amount += amount;
GLOBAL_RATE_LIMIT.save(storage, &state)?;
Ok(())
}
/// 在提现时结合限流
pub fn execute_rate_limited_withdraw(
deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Uint128,
) -> Result<Response, ContractError> {
// 加载配置
let config = RATE_LIMIT_CONFIG.load(deps.storage)?;
// 检查用户级别限流
check_and_consume_rate_limit(
deps.storage,
&info.sender,
amount,
env.block.time,
&config,
)?;
// 检查全局限流
check_global_rate_limit(deps.storage, amount, env.block.time, &config)?;
// 执行业务(检查余额、更新状态、发送资金)
let balance = BALANCES.load(deps.storage, &info.sender)?;
if balance < amount {
return Err(ContractError::InsufficientFunds {});
}
let new_balance = balance - amount;
BALANCES.save(deps.storage, &info.sender, &new_balance)?;
let transfer = BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![coin(amount.u128(), "umsg")],
};
Ok(Response::new()
.add_message(transfer)
.add_attribute("action", "rate_limited_withdraw")
.add_attribute("amount", amount))
}
9. 完整示例对比
9.1 场景:代币质押奖励合约
以下是同一个需求在使用设计模式前后的对比——简易版 vs 开发参考级版。
9.2 需求描述
- 用户可质押
umsg代币 - 每秒按质押比例发放奖励
- 支持领取累积奖励
- 合约可升级(未来支持多币种奖励)
- 支持紧急暂停
- 有提现速率限制
- 所有权可安全转移
9.3 反例:未使用设计模式(简易版)
/// ⚠️ 反例:以下是存在安全问题的简易实现
use cosmwasm_std::{
coin, to_binary, BankMsg, Binary, Coin, Deps, DepsMut, Env, MessageInfo,
QueryRequest, Response, StdResult, Uint128, WasmMsg, WasmQuery,
};
use cw_storage_plus::{Item, Map};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct State {
pub owner: String,
pub total_staked: Uint128,
pub reward_rate: Uint128, // 每秒奖励
pub last_update: u64, // 上次更新时间戳
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct UserInfo {
pub staked: Uint128,
pub rewards: Uint128, // ⚠️ 直接存储累积奖励
pub updated_at: u64,
}
pub const STATE: Item<State> = Item::new("state");
pub const USERS: Map<String, UserInfo> = Map::new("users");
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
_msg: InstantiateMsg,
) -> StdResult<Response> {
let state = State {
owner: info.sender.to_string(),
total_staked: Uint128::zero(),
reward_rate: Uint128::new(1000), // 1000 umsg/s
last_update: 0,
};
STATE.save(deps.storage, &state)?;
Ok(Response::new())
}
/// ⚠️ 问题1: 没有暂停机制
/// ⚠️ 问题2: 没有 CEI 模式
/// ⚠️ 问题3: 没有权限控制
/// ⚠️ 问题4: 奖励计算公式有问题
/// ⚠️ 问题5: 传入 String 而非 Addr
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> StdResult<Response> {
match msg {
ExecuteMsg::Stake { amount } => execute_stake(deps, env, info, amount),
ExecuteMsg::Unstake { amount } => execute_unstake(deps, env, info, amount),
ExecuteMsg::Claim {} => execute_claim(deps, env, info),
ExecuteMsg::UpdateRate { rate } => execute_update_rate(deps, env, info, rate),
}
}
pub fn execute_stake(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Uint128,
) -> StdResult<Response> {
let mut state = STATE.load(deps.storage)?;
let mut user = USERS
.load(deps.storage, &info.sender.to_string())
.unwrap_or(UserInfo {
staked: Uint128::zero(),
rewards: Uint128::zero(),
updated_at: env.block.time.seconds(),
});
// ⚠️ 问题: 尝试计算累积奖励但公式写错了
let elapsed = env.block.time.seconds() - user.updated_at;
// 应该用 state.reward_rate * elapsed * user.staked / state.total_staked
// 但这里直接乘,完全错误
user.rewards += Uint128::from(elapsed) * state.reward_rate;
user.updated_at = env.block.time.seconds();
user.staked += amount;
state.total_staked += amount;
state.last_update = env.block.time.seconds();
STATE.save(deps.storage, &state)?;
USERS.save(deps.storage, &info.sender.to_string(), &user)?;
// ⚠️ 问题: 没有验证用户是否发送了足够的资金
Ok(Response::new()
.add_attribute("action", "stake"))
}
pub fn execute_unstake(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Uint128,
) -> StdResult<Response> {
let mut state = STATE.load(deps.storage)?;
let mut user = USERS.load(deps.storage, &info.sender.to_string())?;
// ⚠️ 问题: 没有检查 CEI 顺序
// 先发钱,再改状态——重入风险
let transfer = BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![coin(amount.u128(), "umsg")],
};
user.staked -= amount; // ⚠️ 如果 transfer 失败,状态已改
state.total_staked -= amount;
STATE.save(deps.storage, &state)?;
USERS.save(deps.storage, &info.sender.to_string(), &user)?;
Ok(Response::new()
.add_message(transfer)
.add_attribute("action", "unstake"))
}
pub fn execute_claim(
mut deps: DepsMut,
env: Env,
info: MessageInfo,
) -> StdResult<Response> {
let mut user = USERS.load(deps.storage, &info.sender.to_string())?;
let state = STATE.load(deps.storage)?;
// ⚠️ 问题: 没有更新累积奖励就计算
let amount = user.rewards;
user.rewards = Uint128::zero();
USERS.save(deps.storage, &info.sender.to_string(), &user)?;
let transfer = BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![coin(amount.u128(), "umsg")],
};
Ok(Response::new()
.add_message(transfer)
.add_attribute("action", "claim"))
}
/// ⚠️ 问题: 任何人都可以调用
pub fn execute_update_rate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
rate: Uint128,
) -> StdResult<Response> {
let mut state = STATE.load(deps.storage)?;
state.reward_rate = rate;
STATE.save(deps.storage, &state)?;
Ok(Response::new()
.add_attribute("action", "update_rate"))
}
9.4 正例:应用设计模式(开发参考级版)
/// ✅ 正例:应用了完整设计模式的开发参考级实现
use cosmwasm_std::{
coin, Decimal, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
Timestamp, Uint128,
};
use cw2::set_contract_version;
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use thiserror::Error;
// ─── 常量 ─────────────────────────────────────────
const CONTRACT_NAME: &str = "crates.io:msg-staking-rewards";
const CONTRACT_VERSION: &str = "2.0.0";
// ─── 错误定义 ─────────────────────────────────────
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),
#[error("Unauthorized")]
Unauthorized,
#[error("Contract is paused: {reason}")]
ContractPaused { reason: String },
#[error("Insufficient balance: balance={balance}, required={required}")]
InsufficientBalance { balance: Uint128, required: Uint128 },
#[error("Insufficient funds")]
InsufficientFunds,
#[error("Zero amount not allowed")]
ZeroAmount,
#[error("No pending reward")]
NoPendingReward,
#[error("Same owner")]
SameOwner,
#[error("Rate limit exceeded")]
RateLimitExceeded,
#[error("Migration error: {0}")]
MigrationError(String),
}
// ─── 存储:版本化(cw2) + 有状态 ────────────────
// 使用 cw2::set_contract_version 追踪合约版本
// ─── 模式1: Owner-only + 两步所有权转移 ──────────
pub const OWNER: Item<Addr> = Item::new("owner");
pub const PENDING_OWNER: Item<Option<Addr>> = Item::new("pending_owner");
// ─── 模式2: 紧急暂停 ──────────────────────────────
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct PauseInfo {
pub paused: bool,
pub paused_by: Option<Addr>,
pub paused_at: Option<u64>,
pub reason: Option<String>,
}
pub const PAUSE_INFO: Item<PauseInfo> = Item::new("pause_info");
pub const PAUSER: Item<Addr> = Item::new("pauser");
// ─── 模式3: 速率限制 ─────────────────────────────
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct RateLimitConfig {
pub window_seconds: u64,
pub max_amount: Uint128,
}
pub const RATE_LIMIT_CONFIG: Item<RateLimitConfig> = Item::new("rlc");
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct RateLimitState {
pub window_start: Timestamp,
pub used_amount: Uint128,
}
pub const RATE_LIMIT_STATES: Map<&Addr, RateLimitState> = Item::new("rls");
// ─── 模式4: 每秒奖励累积 ─────────────────────────
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct GlobalRewardState {
pub accumulated_reward_per_share: Decimal,
pub last_update: Timestamp,
pub reward_rate_per_second: Uint128,
pub total_staked: Uint128,
}
pub const GLOBAL_REWARD: Item<GlobalRewardState> = Item::new("global_reward");
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct UserReward {
pub stake: Uint128,
pub reward_debt: Decimal,
}
pub const USER_REWARDS: Map<&Addr, UserReward> = Map::new("user_rewards");
// ─── 模式5: CEI ───────────────────────────────────
// 所有业务函数严格按照 Checks-Effects-Interactions 顺序
// ─── 实例化 ───────────────────────────────────────
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response> {
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
// Owner
let owner = deps.api.addr_validate(&msg.owner)?;
OWNER.save(deps.storage, &owner)?;
PAUSER.save(deps.storage, &owner)?;
// Pause
PAUSE_INFO.save(
deps.storage,
&PauseInfo {
paused: false,
paused_by: None,
paused_at: None,
reason: None,
},
)?;
// Rate limit
RATE_LIMIT_CONFIG.save(
deps.storage,
&RateLimitConfig {
window_seconds: 86400, // 24h
max_amount: Uint128::new(1_000_000_000_000u128), // 1M MSG
},
)?;
// Reward state
GLOBAL_REWARD.save(
deps.storage,
&GlobalRewardState {
accumulated_reward_per_share: Decimal::zero(),
last_update: _env.block.time,
reward_rate_per_second: msg.initial_reward_rate,
total_staked: Uint128::zero(),
},
)?;
Ok(Response::new()
.add_attribute("action", "instantiate")
.add_attribute("owner", &msg.owner))
}
// ─── 执行入口 ─────────────────────────────────────
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::Stake { amount } => execute_stake(deps, env, info, amount),
ExecuteMsg::Unstake { amount } => execute_unstake(deps, env, info, amount),
ExecuteMsg::Claim {} => execute_claim(deps, env, info),
ExecuteMsg::UpdateConfig { new_rate } => execute_update_config(deps, env, info, new_rate),
ExecuteMsg::Pause { reason } => execute_pause(deps, env, info, reason),
ExecuteMsg::Unpause {} => execute_unpause(deps, env, info),
ExecuteMsg::TransferOwnership { new_owner } => {
execute_transfer_ownership(deps, env, info, new_owner)
}
ExecuteMsg::AcceptOwnership {} => execute_accept_ownership(deps, env, info),
}
}
// ─── 质押 ─────────────────────────────────────────
pub fn execute_stake(
deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Uint128,
) -> Result<Response, ContractError> {
// ── CHECK ────────────────────────────────────
assert_not_paused(deps.storage)?;
if amount.is_zero() {
return Err(ContractError::ZeroAmount);
}
let funds = info
.funds
.iter()
.find(|c| c.denom == "umsg")
.map(|c| c.amount)
.unwrap_or_default();
if funds < amount {
return Err(ContractError::InsufficientFunds);
}
// ── EFFECT ───────────────────────────────────
let mut global = update_global_reward(deps.storage, env.block.time)?;
USER_REWARDS.update(deps.storage, &info.sender, |existing| -> StdResult<_> {
let mut user = existing.unwrap_or(UserReward {
stake: Uint128::zero(),
reward_debt: Decimal::zero(),
});
user.stake += amount;
user.reward_debt = global.accumulated_reward_per_share * user.stake;
Ok(user)
})?;
global.total_staked += amount;
GLOBAL_REWARD.save(deps.storage, &global)?;
// ── INTERACTION ──────────────────────────────
Ok(Response::new()
.add_attribute("action", "stake")
.add_attribute("sender", info.sender)
.add_attribute("amount", amount))
}
// ─── 解除质押 ─────────────────────────────────────
pub fn execute_unstake(
deps: DepsMut,
env: Env,
info: MessageInfo,
amount: Uint128,
) -> Result<Response, ContractError> {
// ── CHECK ────────────────────────────────────
assert_not_paused(deps.storage)?;
check_rate_limit(deps.storage, &info.sender, amount, env.block.time)?;
if amount.is_zero() {
return Err(ContractError::ZeroAmount);
}
let mut global = update_global_reward(deps.storage, env.block.time)?;
let user = USER_REWARDS.load(deps.storage, &info.sender)?;
if user.stake < amount {
return Err(ContractError::InsufficientBalance {
balance: user.stake,
required: amount,
});
}
// ── EFFECT ───────────────────────────────────
USER_REWARDS.save(
deps.storage,
&info.sender,
&UserReward {
stake: user.stake - amount,
reward_debt: global.accumulated_reward_per_share * (user.stake - amount),
},
)?;
global.total_staked -= amount;
GLOBAL_REWARD.save(deps.storage, &global)?;
// ── INTERACTION ──────────────────────────────
let transfer = BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![coin(amount.u128(), "umsg")],
};
Ok(Response::new()
.add_message(transfer)
.add_attribute("action", "unstake")
.add_attribute("amount", amount))
}
// ─── 领取奖励 ─────────────────────────────────────
pub fn execute_claim(
deps: DepsMut,
env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
// ── CHECK ────────────────────────────────────
assert_not_paused(deps.storage)?;
check_rate_limit(deps.storage, &info.sender, Uint128::MAX, env.block.time)?;
let global = update_global_reward(deps.storage, env.block.time)?;
let user = USER_REWARDS.load(deps.storage, &info.sender)?;
let pending = (global.accumulated_reward_per_share * user.stake)
.checked_sub(user.reward_debt)
.map_err(|_| ContractError::NoPendingReward)?;
if pending.is_zero() {
return Err(ContractError::NoPendingReward);
}
// ── EFFECT ───────────────────────────────────
USER_REWARDS.save(
deps.storage,
&info.sender,
&UserReward {
stake: user.stake,
reward_debt: global.accumulated_reward_per_share * user.stake,
},
)?;
// ── INTERACTION ──────────────────────────────
let transfer = BankMsg::Send {
to_address: info.sender.to_string(),
amount: vec![coin(pending.u128(), "umsg")],
};
Ok(Response::new()
.add_message(transfer)
.add_attribute("action", "claim")
.add_attribute("amount", pending))
}
// ─── 管理函数 ─────────────────────────────────────
pub fn execute_update_config(
deps: DepsMut,
env: Env,
info: MessageInfo,
new_rate: Uint128,
) -> Result<Response, ContractError> {
assert_owner(deps.storage, &info.sender)?;
assert_not_paused(deps.storage)?;
let mut global = update_global_reward(deps.storage, env.block.time)?;
global.reward_rate_per_second = new_rate;
GLOBAL_REWARD.save(deps.storage, &global)?;
Ok(Response::new()
.add_attribute("action", "update_config")
.add_attribute("new_rate", new_rate))
}
// ─── 暂停 / 恢复 ─────────────────────────────────
pub fn execute_pause(
deps: DepsMut,
env: Env,
info: MessageInfo,
reason: Option<String>,
) -> Result<Response, ContractError> {
let pauser = PAUSER.load(deps.storage)?;
if info.sender != pauser {
return Err(ContractError::Unauthorized);
}
PAUSE_INFO.save(
deps.storage,
&PauseInfo {
paused: true,
paused_by: Some(info.sender.clone()),
paused_at: Some(env.block.height),
reason,
},
)?;
Ok(Response::new()
.add_attribute("action", "pause"))
}
pub fn execute_unpause(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let pauser = PAUSER.load(deps.storage)?;
if info.sender != pauser {
return Err(ContractError::Unauthorized);
}
PAUSE_INFO.save(
deps.storage,
&PauseInfo {
paused: false,
paused_by: None,
paused_at: None,
reason: None,
},
)?;
Ok(Response::new()
.add_attribute("action", "unpause"))
}
// ─── 两步所有权转移 ──────────────────────────────
pub fn execute_transfer_ownership(
deps: DepsMut,
_env: Env,
info: MessageInfo,
new_owner: Addr,
) -> Result<Response, ContractError> {
assert_owner(deps.storage, &info.sender)?;
let owner = OWNER.load(deps.storage)?;
if new_owner == owner {
return Err(ContractError::SameOwner);
}
PENDING_OWNER.save(deps.storage, &Some(new_owner.clone()))?;
Ok(Response::new()
.add_attribute("action", "transfer_ownership")
.add_attribute("pending_owner", new_owner))
}
pub fn execute_accept_ownership(
deps: DepsMut,
_env: Env,
info: MessageInfo,
) -> Result<Response, ContractError> {
let pending = PENDING_OWNER.load(deps.storage)?;
match pending {
Some(addr) if addr == info.sender => {
let old = OWNER.load(deps.storage)?;
OWNER.save(deps.storage, &info.sender)?;
PENDING_OWNER.save(deps.storage, &None)?;
Ok(Response::new()
.add_attribute("action", "accept_ownership")
.add_attribute("old_owner", old)
.add_attribute("new_owner", info.sender))
}
_ => Err(ContractError::Unauthorized),
}
}
// ─── 守卫函数 ─────────────────────────────────────
pub fn assert_owner(storage: &dyn Storage, sender: &Addr) -> Result<(), ContractError> {
let owner = OWNER.load(storage)?;
if sender != &owner {
return Err(ContractError::Unauthorized);
}
Ok(())
}
pub fn assert_not_paused(storage: &dyn Storage) -> Result<(), ContractError> {
let info = PAUSE_INFO.load(storage)?;
if info.paused {
return Err(ContractError::ContractPaused {
reason: info.reason.unwrap_or_default(),
});
}
Ok(())
}
pub fn check_rate_limit(
storage: &dyn Storage,
addr: &Addr,
_amount: Uint128,
_block_time: Timestamp,
) -> Result<(), ContractError> {
let config = RATE_LIMIT_CONFIG.load(storage)?;
let state = RATE_LIMIT_STATES
.may_load(storage, addr)?
.unwrap_or(RateLimitState {
window_start: _block_time,
used_amount: Uint128::zero(),
});
let elapsed = _block_time.seconds() - state.window_start.seconds();
if elapsed >= config.window_seconds {
return Ok(()); // 窗口已过期,重置
}
let new_total = state.used_amount + _amount;
if new_total > config.max_amount {
return Err(ContractError::RateLimitExceeded);
}
Ok(())
}
// ─── 全局奖励更新 ────────────────────────────────
pub fn update_global_reward(
storage: &mut dyn Storage,
current_time: Timestamp,
) -> StdResult<GlobalRewardState> {
let mut global = GLOBAL_REWARD.load(storage)?;
let elapsed = current_time.seconds() - global.last_update.seconds();
if elapsed > 0 && !global.total_staked.is_zero() {
let reward = global.reward_rate_per_second * Uint128::from(elapsed);
let per_share = Decimal::from_ratio(reward, global.total_staked);
global.accumulated_reward_per_share += per_share;
global.last_update = current_time;
GLOBAL_REWARD.save(storage, &global)?;
} else if elapsed > 0 && global.total_staked.is_zero() {
global.last_update = current_time;
GLOBAL_REWARD.save(storage, &global)?;
}
Ok(global)
}
// ─── 迁移入口 ─────────────────────────────────────
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(
deps: DepsMut,
_env: Env,
msg: MigrateMsg,
) -> Result<Response, ContractError> {
let old_ver = cw2::get_contract_version(deps.storage)?;
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
// 按需执行存储迁移
match old_ver.version.as_str() {
"1.0.0" => {
// V1 -> V2 迁移逻辑
}
_ => {}
}
Ok(Response::new()
.add_attribute("action", "migrate")
.add_attribute("from", old_ver.version)
.add_attribute("to", CONTRACT_VERSION))
}
9.5 对比总结
| 维度 | 简易版(反例) | 开发参考级版(正例) |
|---|---|---|
| 权限控制 | ❌ 任何人都可改奖励率 | ✅ 两步所有权转移 + Owner-only |
| 暂停机制 | ❌ 无 | ✅ 紧急暂停断路器 |
| 速率限制 | ❌ 无 | ✅ 按时间窗口限制提现 |
| 存储效率 | ❌ 直接存累积值,GAS 高 | ✅ 使用 Decimal 累积份额,O(1) 更新 |
| 重入防护 | ❌ CEI 顺序错误 | ✅ 严格 CEI 模式 |
| 类型安全 | ❌ 使用 String 而非 Addr |
✅ 使用 Addr 编译时验证 |
| 可升级性 | ❌ 无版本追踪 | ✅ cw2 版本控制 + MigrateMsg |
| 奖励计算 | ❌ 公式错误,不公平 | ✅ 使用标准 reward_debt 算法 |
| 错误处理 | ❌ 泛化 StdError | ✅ 细粒度 ContractError 枚举 |
| 地址验证 | ❌ 未验证 | ✅ api.addr_validate() |
附录
A. MSG Chain 快速参考
| 项目 | 值 |
|---|---|
| Chain ID | msg-chain-1 |
| Bech32 前缀 | msg |
| 原生代币 | umsg (1 MSG = 10^18 umsg) |
| CosmWasm 版本 | 1.x |
| cw-storage-plus | 1.x |
| RPC 端点 | https://rpc.msg-chain-1.xxx |
| 浏览器 | https://explorer.msg-chain-1.xxx |
B. 推荐依赖
[package]
name = "my-contract"
version = "0.1.0"
edition = "2021"
[dependencies]
cosmwasm-std = "1.5"
cosmwasm-schema = "1.5"
cw-storage-plus = "1.2"
cw-utils = "1.0"
cw2 = "1.1"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"
[dev-dependencies]
cosmwasm-vm = "1.5"
本文档持续更新。欢迎 PR 贡献更多的设计模式与实践案例。
