dApp Docs/智能合约设计模式大全
Development reference. Not independently verified for production.

CosmWasm 智能合约设计模式大全

数据来源:MSG Chain 代码库核实

主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。


目录

  1. 概述
  2. 存储模式
  3. 授权模式
  4. 工厂模式
  5. 回调模式
  6. 升级模式
  7. 经济模式
  8. 安全模式
  9. 完整示例对比

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 特定注意事项


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 需求描述

  1. 用户可质押 umsg 代币
  2. 每秒按质押比例发放奖励
  3. 支持领取累积奖励
  4. 合约可升级(未来支持多币种奖励)
  5. 支持紧急暂停
  6. 有提现速率限制
  7. 所有权可安全转移

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 贡献更多的设计模式与实践案例。