dApp Docs/CW1(Subkeys)合约实现
Development reference. Not independently verified for production.

CW1 (Subkeys/Allowance) 合约实现指南

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

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

目标链: MSG Chain (msg-chain-1)
Bech32 前缀: msg
标准版本: CosmWasm CW1 (账户委托/津贴)


目录

  1. 概述
  2. cw1_whitelist 实现
  3. cw1_subkeys 实现
  4. 集成场景
  5. 安全考虑
  6. 附录

1. 概述

1.1 什么是 CW1

CW1 是 CosmWasm 生态中用于 账户委托 (Account Delegation) 的标准接口。其核心思想是:

主账户 (Admin)
  ├── 授予子密钥 (Sub-key) 有限的资金使用权限
  └── 子密钥代表主账户执行交易,但不能转移超出限额的资金

CW1 合约本质上是一个 代理合约:外部账户将代币存入合约,然后授权其他地址(子密钥)从该合约中支出代币,支出上限由主账户预先设定。

1.2 核心概念

概念 说明
Admin (管理员) 合约的创建者/所有者,拥有最高权限
Sub-key (子密钥) 被授权可以从合约支出代币的地址
Allowance (津贴/限额) 子密钥被允许支出的最大代币数量
Expiry (过期时间) 津贴的有效截止时间,过期后自动失效
Whitelist (白名单) 允许转账的目标地址列表(仅用于 cw1_whitelist)

1.3 使用场景

Gas Relayer (燃料中继器)

用户 A (无 MSG)
  └── 签署交易 → 发送给 Relayer
      └── Relayer (子密钥) 从 A 的合约中扣除 Gas 费用
          └── 将实际交易提交到 MSG Chain

这是 CW1 最常见的应用场景。用户可以在合约中预存 MSG 代币,授权 Relayer 服务从中扣除交易手续费,而无需用户持有原生代币。

自动订阅支付 (Subscription Payments)

用户
  └── 授权 SaaS 平台合约地址
      └── 每月自动扣除订阅费用 (限额内)
          └── 无需用户手动发起每笔交易

委托交易 (Delegated Trading)

大户
  └── 授权交易机器人地址
      ├── 日交易限额: 10,000 MSG
      └── 交易对限制: 仅 USDC/MSG 交易对

团队资金管理

DAO 国库
  ├── 运营地址: 月限额 5,000 MSG
  ├── 市场地址: 月限额 10,000 MSG
  └── 开发地址: 月限额 3,000 MSG

1.4 cw1_whitelist vs cw1_subkeys

特性 cw1_whitelist cw1_subkeys
授权对象 白名单中的所有地址 每个子密钥独立配置
限额控制 所有子密钥共享总余额 每个子密钥有独立限额
过期时间 不支持 支持
增发/减少限额 不支持 (一次性设置) 支持动态调整
目标地址限制 限定只能转账到白名单地址 不限目标地址
适用场景 简单的 Gas relayer 复杂的授权管理

选择建议:

1.5 CW1 与其他 CW 标准的区别

标准 功能 区别
CW1 账户委托/津贴 控制"谁可以从我的账户花多少钱"
CW20 代币标准 类似 ERC-20 的同质化代币
CW3 多重签名 多个签名者共同决策
CW4 分组管理 管理地址列表和权重
CW4626 代币化金库 收益聚合标准

1.6 MSG Chain 网络配置

{
  "chainId": "msg-chain-1",
  "bech32Prefix": "msg",
  "rpcUrl": "https://rpc.msgchain.org",
  "restUrl": "https://rest.msgchain.org",
  "gasPrice": "1000000000attoMSG",
  "denom": "umsg"
}

1.7 依赖与工具链

[dependencies]
cosmwasm-std = "2.0"
cosmwasm-storage = "2.0"
cw-storage-plus = "2.0"
cw-utils = "2.0"
schemars = "0.8"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"

[dev-dependencies]
cosmwasm-schema = "2.0"
cw-multi-test = "2.0"

合约编译:

# 安装 wasm 编译目标
rustup target add wasm32-unknown-unknown

# 编译合约
cargo wasm

# 优化体积 (推荐)
docker run --rm -v "$(pwd)":/code \
  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \
  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
  cosmwasm/optimizer:0.16

# 部署到 MSG Chain
msg-chain-devkit tx wasm store ./artifacts/cw1_subkeys.wasm \
  --from deployer \
  --gas auto \
  --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org

2. cw1_whitelist 实现

2.1 概述

cw1_whitelist 是最简单的 CW1 实现。它的核心规则:

  1. Admin 可以随时添加/移除白名单地址
  2. 白名单中的地址 可以从合约中转出任意金额的代币(但仅限转账到白名单中定义的目标地址)
  3. 非白名单地址 没有任何权限

2.2 完整实现

2.2.1 状态定义

// src/state.rs
use cosmwasm_std::Addr;
use cw_storage_plus::Item;

/// 合约状态
/// - admin: 合约管理员地址
/// - allowed: 允许的目标地址列表(只有这些地址才能接收转账)
pub struct Cw1Whitelist {
    pub admin: Addr,
    pub allowed: Vec<Addr>,
}

/// 存储键
pub const CW1_WHITELIST: Item<Cw1Whitelist> = Item::new("cw1_whitelist");

2.2.2 消息定义

// src/msg.rs
use cosmwasm_std::{Addr, Coin};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// 实例化消息
/// - admin: 管理员地址(如果不填,默认为发送者)
/// - allowed: 允许转账的目标地址列表
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct InstantiateMsg {
    pub admin: Option<String>,
    pub allowed: Vec<String>,
}

/// 执行消息
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    /// 执行转账操作(仅子密钥可调用)
    Execute {
        /// 转账消息列表
        msgs: Vec<cosmwasm_std::Msg>,
    },
    /// 更新白名单(仅管理员可调用)
    UpdateAllowed {
        /// 新的白名单地址列表
        allowed: Vec<String>,
    },
    /// 转移管理员权限(仅管理员可调用)
    UpdateAdmin {
        /// 新管理员地址
        admin: String,
    },
    /// 冻结合约(仅管理员可调用)
    Freeze {},
}

/// 查询消息
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    /// 查询管理员地址
    Admin {},
    /// 查询白名单列表
    Allowed {},
    /// 查询是否可以执行(子密钥查询是否可转账给某地址)
    CanExecute {
        /// 发送者地址
        sender: String,
        /// 转账消息
        msg: cosmwasm_std::Msg,
    },
}

/// 管理员响应
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct AdminResponse {
    pub admin: String,
}

/// 白名单列表响应
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct AllowedResponse {
    pub allowed: Vec<String>,
}

/// 可执行检查响应
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct CanExecuteResponse {
    pub can_execute: bool,
}

2.2.3 错误定义

// src/error.rs
use cosmwasm_std::StdError;
use thiserror::Error;

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

    #[error("Unauthorized: sender is not the contract admin")]
    UnauthorizedAdmin {},

    #[error("Unauthorized: sender is not a whitelisted sub-key")]
    UnauthorizedSubKey {},

    #[error("The contract is frozen and no actions are permitted")]
    Frozen {},

    #[error("No allowed targets configured")]
    NoAllowedTargets {},

    #[error("Target address {target} is not in the whitelist")]
    TargetNotAllowed { target: String },

    #[error("Empty messages list")]
    EmptyMessages {},
}

2.2.4 合约入口

// src/contract.rs
use cosmwasm_std::{
    entry_point, to_json_binary, Addr, Binary, Coin, CosmosMsg, Deps, DepsMut,
    Env, MessageInfo, Response, StdResult, WasmMsg, BankMsg, SubMsg,
};
use crate::error::ContractError;
use crate::msg::{
    AdminResponse, AllowedResponse, CanExecuteResponse,
    ExecuteMsg, InstantiateMsg, QueryMsg,
};
use crate::state::{Cw1Whitelist, CW1_WHITELIST};

/// 合约实例化
/// 创建一个新的 cw1_whitelist 合约实例
#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    // 确定管理员地址
    let admin = match msg.admin {
        Some(a) => deps.api.addr_validate(&a)?,
        None => info.sender.clone(),
    };

    // 验证所有白名单地址
    let allowed: Vec<Addr> = msg
        .allowed
        .iter()
        .map(|a| deps.api.addr_validate(a))
        .collect::<StdResult<Vec<Addr>>>()?;

    // 保存状态
    let state = Cw1Whitelist { admin, allowed };
    CW1_WHITELIST.save(deps.storage, &state)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("admin", state.admin.to_string())
        .add_attribute("allowed_count", state.allowed.len().to_string()))
}

/// 执行入口
#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Execute { msgs } => execute_execute(deps, env, info, msgs),
        ExecuteMsg::UpdateAllowed { allowed } => execute_update_allowed(deps, env, info, allowed),
        ExecuteMsg::UpdateAdmin { admin } => execute_update_admin(deps, env, info, admin),
        ExecuteMsg::Freeze {} => execute_freeze(deps, env, info),
    }
}

/// 执行转账操作
/// 只有白名单中的子密钥可以调用此方法
fn execute_execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msgs: Vec<CosmosMsg>,
) -> Result<Response, ContractError> {
    // 获取合约状态
    let state = CW1_WHITELIST.load(deps.storage)?;

    // 检查合约是否冻结
    // 注意: 此处我们通过状态检查来模拟冻结功能
    // 实际项目中建议使用独立的 IsFrozen 标志

    // 验证调用者是否为白名单子密钥
    // 子密钥定义: 发起调用的地址必须是合约本身
    // 外部账户直接调用时, info.sender 就是调用者
    // 但对于 CW1, 子密钥通常以 "proxy" 方式调用:
    // 子密钥签署交易, 调用 execute 函数, 合约代表 admin 发送消息
    //
    // 进一步检查: 子密钥必须在 allowed 列表中
    let is_allowed = state.allowed.contains(&info.sender);
    if !is_allowed {
        return Err(ContractError::UnauthorizedSubKey {});
    }

    // 验证消息列表非空
    if msgs.is_empty() {
        return Err(ContractError::EmptyMessages {});
    }

    // 验证所有消息的目标地址都在白名单中
    for msg in &msgs {
        match msg {
            CosmosMsg::Bank(bank_msg) => {
                if let BankMsg::Send { to_address, .. } = bank_msg {
                    let to_addr = deps.api.addr_validate(to_address)?;
                    if !state.allowed.contains(&to_addr) {
                        return Err(ContractError::TargetNotAllowed {
                            target: to_address.clone(),
                        });
                    }
                }
            }
            CosmosMsg::Wasm(wasm_msg) => {
                if let WasmMsg::Execute { contract_addr, .. } = wasm_msg {
                    let contract = deps.api.addr_validate(contract_addr)?;
                    if !state.allowed.contains(&contract) {
                        return Err(ContractError::TargetNotAllowed {
                            target: contract_addr.clone(),
                        });
                    }
                }
            }
            _ => {
                // 对其他类型的消息不做白名单限制
                // 例如 StakingMsg, DistributionMsg, IbcMsg 等
                // 但建议对它们也做限制,取决于业务需求
            }
        }
    }

    // 构建响应,包含子消息
    let mut response = Response::new();
    for msg in msgs {
        response = response.add_message(msg);
    }

    Ok(response
        .add_attribute("method", "execute")
        .add_attribute("sender", info.sender)
        .add_attribute("action", "proxy_call"))
}

/// 更新白名单列表
/// 仅管理员可调用
fn execute_update_allowed(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    allowed: Vec<String>,
) -> Result<Response, ContractError> {
    // 验证管理员身份
    let state = CW1_WHITELIST.load(deps.storage)?;
    if info.sender != state.admin {
        return Err(ContractError::UnauthorizedAdmin {});
    }

    // 验证新的白名单地址
    let new_allowed: Vec<Addr> = allowed
        .iter()
        .map(|a| deps.api.addr_validate(a))
        .collect::<StdResult<Vec<Addr>>>()?;

    // 更新状态
    let new_state = Cw1Whitelist {
        admin: state.admin,
        allowed: new_allowed,
    };
    CW1_WHITELIST.save(deps.storage, &new_state)?;

    Ok(Response::new()
        .add_attribute("method", "update_allowed")
        .add_attribute("admin", info.sender)
        .add_attribute("new_allowed_count", new_state.allowed.len().to_string()))
}

/// 转移管理员权限
/// 仅当前管理员可调用
fn execute_update_admin(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    admin: String,
) -> Result<Response, ContractError> {
    // 验证管理员身份
    let state = CW1_WHITELIST.load(deps.storage)?;
    if info.sender != state.admin {
        return Err(ContractError::UnauthorizedAdmin {});
    }

    // 验证新管理员地址
    let new_admin = deps.api.addr_validate(&admin)?;

    // 更新状态
    let new_state = Cw1Whitelist {
        admin: new_admin,
        allowed: state.allowed,
    };
    CW1_WHITELIST.save(deps.storage, &new_state)?;

    Ok(Response::new()
        .add_attribute("method", "update_admin")
        .add_attribute("old_admin", info.sender)
        .add_attribute("new_admin", admin))
}

/// 冻结合约
/// 将允许列表清空,等同于禁用所有子密钥
fn execute_freeze(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
) -> Result<Response, ContractError> {
    // 验证管理员身份
    let state = CW1_WHITELIST.load(deps.storage)?;
    if info.sender != state.admin {
        return Err(ContractError::UnauthorizedAdmin {});
    }

    // 清空白名单 = 冻结
    let new_state = Cw1Whitelist {
        admin: state.admin,
        allowed: vec![],
    };
    CW1_WHITELIST.save(deps.storage, &new_state)?;

    Ok(Response::new()
        .add_attribute("method", "freeze")
        .add_attribute("admin", info.sender))
}

/// 查询入口
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::Admin {} => to_json_binary(&query_admin(deps)?),
        QueryMsg::Allowed {} => to_json_binary(&query_allowed(deps)?),
        QueryMsg::CanExecute { sender, msg } => {
            to_json_binary(&query_can_execute(deps, sender, msg)?)
        }
    }
}

/// 查询管理员地址
fn query_admin(deps: Deps) -> StdResult<AdminResponse> {
    let state = CW1_WHITELIST.load(deps.storage)?;
    Ok(AdminResponse {
        admin: state.admin.to_string(),
    })
}

/// 查询白名单列表
fn query_allowed(deps: Deps) -> StdResult<AllowedResponse> {
    let state = CW1_WHITELIST.load(deps.storage)?;
    Ok(AllowedResponse {
        allowed: state.allowed.iter().map(|a| a.to_string()).collect(),
    })
}

/// 检查指定发送者是否可以向指定目标执行转账
fn query_can_execute(
    deps: Deps,
    sender: String,
    msg: cosmwasm_std::Msg,
) -> StdResult<CanExecuteResponse> {
    let state = CW1_WHITELIST.load(deps.storage)?;

    // 验证发送者
    let sender_addr = deps.api.addr_validate(&sender)?;
    if !state.allowed.contains(&sender_addr) {
        return Ok(CanExecuteResponse {
            can_execute: false,
        });
    }

    // 验证消息目标地址
    match &msg {
        cosmwasm_std::Msg::Bank(bank_msg) => {
            if let BankMsg::Send { to_address, .. } = bank_msg {
                let to_addr = deps.api.addr_validate(to_address)?;
                if !state.allowed.contains(&to_addr) {
                    return Ok(CanExecuteResponse {
                        can_execute: false,
                    });
                }
            }
        }
        _ => {}
    }

    Ok(CanExecuteResponse { can_execute: true })
}

2.2.5 Lib 入口

// src/lib.rs
pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

pub use crate::error::ContractError;
pub use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
pub use crate::state::Cw1Whitelist;

2.2.6 Cargo.toml

[package]
name = "cw1-whitelist"
version = "0.1.0"
edition = "2021"
description = "CW1 Whitelist implementation for MSG Chain"

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

[dependencies]
cosmwasm-std = { version = "2.0", features = ["staking"] }
cw-storage-plus = "2.0"
schemars = "0.8"
serde = { version = "1.0", default-features = false, features = ["derive"] }
thiserror = "1.0"

[dev-dependencies]
cosmwasm-schema = "2.0"
cw-multi-test = "2.0"
cosmwasm-std = { version = "2.0", features = ["staking"] }

2.3 测试实现

// tests/integration.rs
use cosmwasm_std::{
    coin, coins, from_json,
    testing::{mock_dependencies, mock_env, mock_info},
    Addr, BankMsg, Coin, CosmosMsg, WasmMsg,
};
use cw1_whitelist::{
    contract::{execute, instantiate, query},
    error::ContractError,
    msg::{
        AdminResponse, AllowedResponse, CanExecuteResponse,
        ExecuteMsg, InstantiateMsg, QueryMsg,
    },
};

/// 辅助函数:创建合约实例
fn setup_contract(
    admin: Option<&str>,
    allowed: Vec<&str>,
) -> (
    cosmwasm_std::OwnedDeps<
        cosmwasm_std::MemoryStorage,
        cosmwasm_std::testing::MockApi,
        cosmwasm_std::testing::MockQuerier,
    >,
    cosmwasm_std::Env,
) {
    let mut deps = mock_dependencies();
    let env = mock_env();

    let msg = InstantiateMsg {
        admin: admin.map(|s| s.to_string()),
        allowed: allowed.iter().map(|s| s.to_string()).collect(),
    };

    let info = mock_info("creator", &[]);
    instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();

    (deps, env)
}

#[test]
fn test_instantiate_with_default_admin() {
    let (deps, _) = setup_contract(None, vec!["subkey1", "subkey2"]);

    // 查询管理员
    let admin_resp: AdminResponse = from_json(
        query(deps.as_ref(), mock_env(), QueryMsg::Admin {}).unwrap(),
    )
    .unwrap();
    assert_eq!(admin_resp.admin, "creator");

    // 查询白名单
    let allowed_resp: AllowedResponse = from_json(
        query(deps.as_ref(), mock_env(), QueryMsg::Allowed {}).unwrap(),
    )
    .unwrap();
    assert_eq!(allowed_resp.allowed.len(), 2);
    assert!(allowed_resp.allowed.contains(&"subkey1".to_string()));
    assert!(allowed_resp.allowed.contains(&"subkey2".to_string()));
}

#[test]
fn test_instantiate_with_custom_admin() {
    let (deps, _) = setup_contract(Some("custom_admin"), vec![]);

    let admin_resp: AdminResponse = from_json(
        query(deps.as_ref(), mock_env(), QueryMsg::Admin {}).unwrap(),
    )
    .unwrap();
    assert_eq!(admin_resp.admin, "custom_admin");
}

#[test]
fn test_execute_by_allowed_subkey() {
    let (mut deps, env) = setup_contract(None, vec!["subkey1", "recipient"]);

    let msgs = vec![BankMsg::Send {
        to_address: "recipient".to_string(),
        amount: coins(1000, "umsg"),
    }
    .into()];

    let info = mock_info("subkey1", &[]);
    let resp = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs }).unwrap();

    assert_eq!(resp.messages.len(), 1);
}

#[test]
fn test_execute_by_unauthorized_sender() {
    let (mut deps, env) = setup_contract(None, vec!["subkey1", "recipient"]);

    let msgs = vec![BankMsg::Send {
        to_address: "recipient".to_string(),
        amount: coins(100, "umsg"),
    }
    .into()];

    let info = mock_info("attacker", &[]);
    let err = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs }).unwrap_err();

    assert_eq!(err, ContractError::UnauthorizedSubKey {});
}

#[test]
fn test_execute_to_non_whitelisted_target() {
    let (mut deps, env) = setup_contract(None, vec!["subkey1"]);

    let msgs = vec![BankMsg::Send {
        to_address: "unknown_recipient".to_string(),
        amount: coins(100, "umsg"),
    }
    .into()];

    let info = mock_info("subkey1", &[]);
    let err = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs }).unwrap_err();

    assert_eq!(
        err,
        ContractError::TargetNotAllowed {
            target: "unknown_recipient".to_string()
        }
    );
}

#[test]
fn test_execute_with_empty_messages() {
    let (mut deps, env) = setup_contract(None, vec!["subkey1"]);

    let info = mock_info("subkey1", &[]);
    let err = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs: vec![] })
        .unwrap_err();

    assert_eq!(err, ContractError::EmptyMessages {});
}

#[test]
fn test_update_allowed_as_admin() {
    let (mut deps, env) = setup_contract(None, vec!["subkey1"]);

    // 更新白名单
    let info = mock_info("creator", &[]);
    execute(
        deps.as_mut(),
        env.clone(),
        info,
        ExecuteMsg::UpdateAllowed {
            allowed: vec!["new_subkey".to_string(), "new_target".to_string()],
        },
    )
    .unwrap();

    // 验证更新结果
    let allowed_resp: AllowedResponse = from_json(
        query(deps.as_ref(), env, QueryMsg::Allowed {}).unwrap(),
    )
    .unwrap();
    assert_eq!(allowed_resp.allowed.len(), 2);
    assert!(allowed_resp.allowed.contains(&"new_subkey".to_string()));
    assert!(allowed_resp.allowed.contains(&"new_target".to_string()));
}

#[test]
fn test_update_allowed_as_non_admin() {
    let (mut deps, env) = setup_contract(None, vec!["subkey1"]);

    let info = mock_info("attacker", &[]);
    let err = execute(
        deps.as_mut(),
        env,
        info,
        ExecuteMsg::UpdateAllowed {
            allowed: vec!["hacker".to_string()],
        },
    )
    .unwrap_err();

    assert_eq!(err, ContractError::UnauthorizedAdmin {});
}

#[test]
fn test_update_admin() {
    let (mut deps, env) = setup_contract(None, vec![]);

    // 转移管理权限
    let info = mock_info("creator", &[]);
    execute(
        deps.as_mut(),
        env.clone(),
        info,
        ExecuteMsg::UpdateAdmin {
            admin: "new_admin".to_string(),
        },
    )
    .unwrap();

    // 验证新管理员
    let admin_resp: AdminResponse = from_json(
        query(deps.as_ref(), env, QueryMsg::Admin {}).unwrap(),
    )
    .unwrap();
    assert_eq!(admin_resp.admin, "new_admin");
}

#[test]
fn test_update_admin_by_non_admin() {
    let (mut deps, env) = setup_contract(None, vec![]);

    let info = mock_info("attacker", &[]);
    let err = execute(
        deps.as_mut(),
        env,
        info,
        ExecuteMsg::UpdateAdmin {
            admin: "hacker_admin".to_string(),
        },
    )
    .unwrap_err();

    assert_eq!(err, ContractError::UnauthorizedAdmin {});
}

#[test]
fn test_freeze_contract() {
    let (mut deps, env) = setup_contract(None, vec!["subkey1", "recipient"]);

    // 冻结合约
    let info = mock_info("creator", &[]);
    execute(deps.as_mut(), env.clone(), info, ExecuteMsg::Freeze {}).unwrap();

    // 验证白名单已清空
    let allowed_resp: AllowedResponse = from_json(
        query(deps.as_ref(), env, QueryMsg::Allowed {}).unwrap(),
    )
    .unwrap();
    assert!(allowed_resp.allowed.is_empty());
}

#[test]
fn test_freeze_by_non_admin() {
    let (mut deps, env) = setup_contract(None, vec!["subkey1"]);

    let info = mock_info("attacker", &[]);
    let err = execute(deps.as_mut(), env, info, ExecuteMsg::Freeze {}).unwrap_err();

    assert_eq!(err, ContractError::UnauthorizedAdmin {});
}

#[test]
fn test_can_execute_query() {
    let (deps, _) = setup_contract(None, vec!["subkey1", "recipient"]);

    // 子密钥可以向白名单地址转账
    let resp: CanExecuteResponse = from_json(
        query(
            deps.as_ref(),
            mock_env(),
            QueryMsg::CanExecute {
                sender: "subkey1".to_string(),
                msg: BankMsg::Send {
                    to_address: "recipient".to_string(),
                    amount: coins(500, "umsg"),
                }
                .into(),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert!(resp.can_execute);

    // 未经授权的发送者
    let resp: CanExecuteResponse = from_json(
        query(
            deps.as_ref(),
            mock_env(),
            QueryMsg::CanExecute {
                sender: "attacker".to_string(),
                msg: BankMsg::Send {
                    to_address: "recipient".to_string(),
                    amount: coins(500, "umsg"),
                }
                .into(),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert!(!resp.can_execute);

    // 目标地址不在白名单中
    let resp: CanExecuteResponse = from_json(
        query(
            deps.as_ref(),
            mock_env(),
            QueryMsg::CanExecute {
                sender: "subkey1".to_string(),
                msg: BankMsg::Send {
                    to_address: "unknown".to_string(),
                    amount: coins(500, "umsg"),
                }
                .into(),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert!(!resp.can_execute);
}

#[test]
fn test_wasm_execute_to_whitelisted_contract() {
    let (mut deps, env) = setup_contract(None, vec!["subkey1", "contract_addr"]);

    let msgs = vec![WasmMsg::Execute {
        contract_addr: "contract_addr".to_string(),
        msg: Binary::from(b"{\"some\":\"action\"}" as &[u8]),
        funds: coins(200, "umsg"),
    }
    .into()];

    let info = mock_info("subkey1", &[]);
    let resp = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs }).unwrap();
    assert_eq!(resp.messages.len(), 1);
}

#[test]
fn test_multiple_messages_in_single_execute() {
    let (mut deps, env) = setup_contract(None, vec!["subkey1", "recipient1", "recipient2"]);

    let msgs = vec![
        BankMsg::Send {
            to_address: "recipient1".to_string(),
            amount: coins(100, "umsg"),
        }
        .into(),
        BankMsg::Send {
            to_address: "recipient2".to_string(),
            amount: coins(200, "umsg"),
        }
        .into(),
    ];

    let info = mock_info("subkey1", &[]);
    let resp = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs }).unwrap();
    assert_eq!(resp.messages.len(), 2);
}

2.4 部署与交互

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

# 2. 存储合约代码
RES=$(msg-chain-devkit tx wasm store artifacts/cw1_whitelist.wasm \
  --from admin \
  --gas auto \
  --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org \
  --output json)
CODE_ID=$(echo $RES | jq -r '.logs[0].events[] | select(.type == "store_code") | .attributes[] | select(.key == "code_id") | .value')
echo "Code ID: $CODE_ID"

# 3. 实例化合约
INIT='{"admin": "msg1admin...", "allowed": ["msg1relayer...", "msg1service..."]}'
msg-chain-devkit tx wasm instantiate $CODE_ID "$INIT" \
  --from admin \
  --label "cw1_whitelist_gas_station" \
  --gas auto \
  --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org \
  --output json
CONTRACT_ADDR=$(echo $RES | jq -r '.logs[0].events[] | select(.type == "instantiate") | .attributes[] | select(.key == "_contract_address") | .value')
echo "Contract: $CONTRACT_ADDR"

# 4. 存入资金(Admin 转入代币)
msg-chain-devkit tx bank send admin $CONTRACT_ADDR 1000000umsg \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org

# 5. 查询合约余额
msg-chain-devkit query bank balances $CONTRACT_ADDR \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org

# 6. 子密钥执行转账
EXECUTE_MSG='{"execute":{"msgs":[{"bank":{"send":{"to_address":"msg1recipient...","amount":[{"denom":"umsg","amount":"500"}]}}}]}}'
msg-chain-devkit tx wasm execute $CONTRACT_ADDR "$EXECUTE_MSG" \
  --from relayer \
  --gas auto \
  --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org

# 7. 查询
msg-chain-devkit query wasm contract-state smart $CONTRACT_ADDR '{"admin":{}}'
msg-chain-devkit query wasm contract-state smart $CONTRACT_ADDR '{"allowed":{}}'
msg-chain-devkit query wasm contract-state smart $CONTRACT_ADDR '{"can_execute":{"sender":"msg1relayer...","msg":{"bank":{"send":{"to_address":"msg1recipient...","amount":[{"denom":"umsg","amount":"500"}]}}}}}'

2.5 cw1_whitelist 优缺点

优点:

缺点:


3. cw1_subkeys 实现

3.1 概述

cw1_subkeys 是 cw1_whitelist 的增强版。它支持:

  1. 每个子密钥独立的津贴(Allowance):包括金额限制和过期时间
  2. 动态调整津贴:管理员可以增加或减少特定子密钥的限额
  3. 支出跟踪:追踪每个子密钥的已使用额度
  4. 过期管理:津贴可以设置过期时间,到期自动失效

3.2 核心数据结构

/// 津贴定义
/// - balance: 剩余可用额度
/// - expires: 过期时间(高度或时间)
/// - description: 备注说明(可选)
pub struct Allowance {
    pub balance: Uint128,
    pub expires: Expiration,
    pub description: Option<String>,
}

/// 合约状态
pub struct Cw1Subkeys {
    pub admin: Addr,
    pub allowances: Map<&'static Addr, Allowance>,
}

3.3 完整实现

3.3.1 状态定义

// src/state.rs
use cosmwasm_std::{Addr, Uint128};
use cw_storage_plus::{Item, Map};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

/// 过期时间定义
/// - AtHeight: 在指定区块高度后过期
/// - AtTime: 在指定时间戳(纳秒)后过期
/// - Never: 永不过期
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub enum Expiration {
    /// 在指定高度过期
    AtHeight(u64),
    /// 在指定时间(纳秒)过期
    AtTime(u64),
    /// 永不过期
    Never {},
}

impl Expiration {
    /// 检查是否已经过期
    pub fn is_expired(&self, height: u64, time_nanos: u64) -> bool {
        match self {
            Expiration::AtHeight(h) => height >= *h,
            Expiration::AtTime(t) => time_nanos >= *t,
            Expiration::Never {} => false,
        }
    }
}

/// 津贴结构
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Allowance {
    /// 剩余可用余额
    pub balance: Uint128,
    /// 过期条件
    pub expires: Expiration,
    /// 备注说明
    pub description: Option<String>,
}

impl Allowance {
    /// 创建一个新的津贴
    pub fn new(balance: Uint128, expires: Expiration, description: Option<String>) -> Self {
        Allowance {
            balance,
            expires,
            description,
        }
    }

    /// 检查津贴是否有效(未过期且有余额)
    pub fn is_valid(&self, height: u64, time_nanos: u64) -> bool {
        !self.expires.is_expired(height, time_nanos) && !self.balance.is_zero()
    }

    /// 扣除指定金额,返回扣除后的新津贴
    pub fn deduct(&self, amount: Uint128) -> Result<Self, ContractError> {
        if amount > self.balance {
            return Err(ContractError::InsufficientAllowance {
                available: self.balance,
                required: amount,
            });
        }
        Ok(Allowance {
            balance: self.balance.checked_sub(amount)?,
            expires: self.expires.clone(),
            description: self.description.clone(),
        })
    }

    /// 增加额度
    pub fn increase(&self, amount: Uint128) -> Result<Self, ContractError> {
        Ok(Allowance {
            balance: self.balance.checked_add(amount)?,
            expires: self.expires.clone(),
            description: self.description.clone(),
        })
    }

    /// 减少额度(不能低于零)
    pub fn decrease(&self, amount: Uint128) -> Result<Self, ContractError> {
        if amount > self.balance {
            return Ok(Allowance {
                balance: Uint128::zero(),
                expires: self.expires.clone(),
                description: self.description.clone(),
            });
        }
        Ok(Allowance {
            balance: self.balance.checked_sub(amount)?,
            expires: self.expires.clone(),
            description: self.description.clone(),
        })
    }
}

/// 合约状态
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Cw1SubkeysState {
    pub admin: Addr,
}

/// 存储键
pub const CW1_SUBKEYS: Item<Cw1SubkeysState> = Item::new("cw1_subkeys_state");
pub const ALLOWANCES: Map<&Addr, Allowance> = Map::new("allowances");

3.3.2 消息定义

// src/msg.rs
use cosmwasm_std::{Addr, Coin, CosmosMsg, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::state::{Allowance, Expiration};

/// 实例化消息
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct InstantiateMsg {
    /// 管理员地址(留空则使用发送者)
    pub admin: Option<String>,
}

/// 执行消息
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    /// 子密钥代表管理员执行操作
    Execute {
        /// 要执行的消息列表
        msgs: Vec<CosmosMsg>,
    },
    /// 增加指定子密钥的津贴
    IncreaseAllowance {
        /// 子密钥地址
        spender: String,
        /// 增加的金额
        amount: Uint128,
        /// 过期时间(可选,不填则继承现有设置)
        expires: Option<Expiration>,
    },
    /// 减少指定子密钥的津贴
    DecreaseAllowance {
        /// 子密钥地址
        spender: String,
        /// 减少的金额
        amount: Uint128,
        /// 新的过期时间(可选)
        expires: Option<Expiration>,
    },
    /// 设置子密钥津贴(覆盖现有设置)
    SetAllowance {
        /// 子密钥地址
        spender: String,
        /// 津贴金额
        amount: Uint128,
        /// 过期时间
        expires: Expiration,
        /// 备注说明
        description: Option<String>,
    },
    /// 撤销子密钥的所有权限(将津贴设为零)
    RevokeAllowance {
        /// 子密钥地址
        spender: String,
    },
    /// 转移管理员权限
    UpdateAdmin {
        /// 新管理员地址
        admin: String,
    },
}

/// 查询消息
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    /// 查询管理员地址
    Admin {},
    /// 查询指定子密钥的津贴
    Allowance {
        /// 子密钥地址
        spender: String,
    },
    /// 查询所有子密钥的津贴(支持分页)
    AllAllowances {
        /// 起始键
        start_after: Option<String>,
        /// 每页数量
        limit: Option<u32>,
    },
    /// 检查指定发送者是否可以执行指定消息
    CanExecute {
        /// 发送者地址
        sender: String,
        /// 要执行的消息
        msg: CosmosMsg,
    },
}

// ===== 响应类型 =====

/// 管理员查询响应
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct AdminResponse {
    pub admin: String,
}

/// 津贴查询响应
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct AllowanceResponse {
    pub allowance: Allowance,
}

/// 所有津贴列表响应
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct AllAllowancesResponse {
    pub allowances: Vec<(String, Allowance)>,
}

/// 可执行检查响应
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct CanExecuteResponse {
    pub can_execute: bool,
}

/// 原始 Msg 类型(用于 CosmosMsg 序列化)
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub struct Msg {
    pub bank: Option<BankMsg>,
    pub wasm: Option<WasmMsg>,
    pub staking: Option<StakingMsg>,
    pub distribution: Option<DistributionMsg>,
    pub ibc: Option<IbcMsg>,
}

#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub struct BankMsg {
    pub send: Option<BankSend>,
}

#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct BankSend {
    pub to_address: String,
    pub amount: Vec<Coin>,
}

#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub struct WasmMsg {
    pub execute: Option<WasmExecute>,
}

#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct WasmExecute {
    pub contract_addr: String,
    pub msg: Binary,
    pub funds: Vec<Coin>,
}

3.3.3 错误定义

// src/error.rs
use cosmwasm_std::{StdError, Uint128};
use thiserror::Error;

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

    #[error("Unauthorized: sender is not the contract admin")]
    UnauthorizedAdmin {},

    #[error("Unauthorized: sender is not a sub-key with valid allowance")]
    UnauthorizedSubKey {},

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

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

    #[error("Empty messages list")]
    EmptyMessages {},

    #[error("Allowance overflow")]
    AllowanceOverflow {},

    #[error("Self-delegation not allowed")]
    SelfDelegationNotAllowed {},

    #[error("Invalid zero amount")]
    InvalidZeroAmount {},
}

3.3.4 合约入口

// src/contract.rs
use cosmwasm_std::{
    entry_point, to_json_binary, Addr, Binary, Deps, DepsMut, Env,
    MessageInfo, Order, Response, StdResult, SubMsgResult,
};
use crate::error::ContractError;
use crate::msg::{
    AdminResponse, AllowanceResponse, AllAllowancesResponse,
    CanExecuteResponse, ExecuteMsg, InstantiateMsg, QueryMsg,
};
use crate::state::{Allowance, Cw1SubkeysState, Expiration, ALLOWANCES, CW1_SUBKEYS};

/// 合约实例化
#[entry_point]
pub fn instantiate(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    let admin = match msg.admin {
        Some(a) => deps.api.addr_validate(&a)?,
        None => info.sender,
    };

    let state = Cw1SubkeysState { admin };
    CW1_SUBKEYS.save(deps.storage, &state)?;

    Ok(Response::new()
        .add_attribute("method", "instantiate")
        .add_attribute("admin", state.admin))
}

/// 执行入口
#[entry_point]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> Result<Response, ContractError> {
    match msg {
        ExecuteMsg::Execute { msgs } => execute_execute(deps, env, info, msgs),
        ExecuteMsg::IncreaseAllowance {
            spender,
            amount,
            expires,
        } => execute_increase_allowance(deps, env, info, spender, amount, expires),
        ExecuteMsg::DecreaseAllowance {
            spender,
            amount,
            expires,
        } => execute_decrease_allowance(deps, env, info, spender, amount, expires),
        ExecuteMsg::SetAllowance {
            spender,
            amount,
            expires,
            description,
        } => execute_set_allowance(deps, env, info, spender, amount, expires, description),
        ExecuteMsg::RevokeAllowance { spender } => {
            execute_revoke_allowance(deps, env, info, spender)
        }
        ExecuteMsg::UpdateAdmin { admin } => execute_update_admin(deps, env, info, admin),
    }
}

/// 子密钥执行操作
fn execute_execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msgs: Vec<CosmosMsg>,
) -> Result<Response, ContractError> {
    if msgs.is_empty() {
        return Err(ContractError::EmptyMessages {});
    }

    // 加载合约状态
    let state = CW1_SUBKEYS.load(deps.storage)?;

    // 管理员可以直接执行任何操作(无限制)
    if info.sender == state.admin {
        let mut response = Response::new();
        for msg in msgs {
            response = response.add_message(msg);
        }
        return Ok(response
            .add_attribute("method", "execute")
            .add_attribute("sender", info.sender)
            .add_attribute("role", "admin"));
    }

    // 子密钥执行:检查津贴
    let allowance = ALLOWANCES
        .load(deps.storage, &info.sender)
        .map_err(|_| ContractError::UnauthorizedSubKey {})?;

    // 检查是否过期
    if allowance.expires.is_expired(env.block.height, env.block.time.nanos()) {
        return Err(ContractError::AllowanceExpired {});
    }

    // 计算本次执行的总资金需求
    let mut total_required = Uint128::zero();
    for msg in &msgs {
        let funds = match msg {
            CosmosMsg::Bank(BankMsg::Send { amount, .. }) => {
                let sum: Uint128 = amount
                    .iter()
                    .fold(Uint128::zero(), |acc, c| acc + c.amount);
                sum
            }
            CosmosMsg::Wasm(WasmMsg::Execute { funds, .. }) => {
                funds
                    .iter()
                    .fold(Uint128::zero(), |acc, c| acc + c.amount)
            }
            _ => Uint128::zero(),
        };
        total_required = total_required.checked_add(funds)?;
    }

    // 检查是否有足够的额度
    if total_required > allowance.balance {
        return Err(ContractError::InsufficientAllowance {
            available: allowance.balance,
            required: total_required,
        });
    }

    // 扣除额度
    let updated = allowance.deduct(total_required)?;
    ALLOWANCES.save(deps.storage, &info.sender, &updated)?;

    // 构建响应
    let mut response = Response::new();
    for msg in msgs {
        response = response.add_message(msg);
    }

    Ok(response
        .add_attribute("method", "execute")
        .add_attribute("sender", info.sender)
        .add_attribute("role", "sub_key")
        .add_attribute("spent", total_required.to_string())
        .add_attribute("remaining", updated.balance.to_string()))
}

/// 增加子密钥津贴(仅管理员)
fn execute_increase_allowance(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    spender: String,
    amount: Uint128,
    expires: Option<Expiration>,
) -> Result<Response, ContractError> {
    // 验证管理员身份
    let state = CW1_SUBKEYS.load(deps.storage)?;
    if info.sender != state.admin {
        return Err(ContractError::UnauthorizedAdmin {});
    }

    // 验证金额
    if amount.is_zero() {
        return Err(ContractError::InvalidZeroAmount {});
    }

    // 验证子密钥地址
    let spender_addr = deps.api.addr_validate(&spender)?;

    // 不允许自己给自己授权
    if spender_addr == state.admin {
        return Err(ContractError::SelfDelegationNotAllowed {});
    }

    // 获取现有津贴(如果没有则创建新的)
    let current = ALLOWANCES
        .may_load(deps.storage, &spender_addr)?
        .unwrap_or(Allowance::new(
            Uint128::zero(),
            Expiration::Never {},
            None,
        ));

    // 增加额度
    let updated = Allowance {
        balance: current
            .balance
            .checked_add(amount)
            .map_err(|_| ContractError::AllowanceOverflow {})?,
        expires: expires.unwrap_or(current.expires),
        description: current.description,
    };
    ALLOWANCES.save(deps.storage, &spender_addr, &updated)?;

    Ok(Response::new()
        .add_attribute("method", "increase_allowance")
        .add_attribute("spender", spender)
        .add_attribute("amount_added", amount.to_string())
        .add_attribute("new_balance", updated.balance.to_string()))
}

/// 减少子密钥津贴(仅管理员)
fn execute_decrease_allowance(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    spender: String,
    amount: Uint128,
    expires: Option<Expiration>,
) -> Result<Response, ContractError> {
    // 验证管理员身份
    let state = CW1_SUBKEYS.load(deps.storage)?;
    if info.sender != state.admin {
        return Err(ContractError::UnauthorizedAdmin {});
    }

    // 验证金额
    if amount.is_zero() {
        return Err(ContractError::InvalidZeroAmount {});
    }

    // 验证子密钥地址
    let spender_addr = deps.api.addr_validate(&spender)?;

    // 获取现有津贴
    let current = ALLOWANCES
        .may_load(deps.storage, &spender_addr)?
        .unwrap_or(Allowance::new(
            Uint128::zero(),
            Expiration::Never {},
            None,
        ));

    // 减少额度(不能低于零)
    let new_balance = if amount >= current.balance {
        Uint128::zero()
    } else {
        current.balance.checked_sub(amount)?
    };

    let updated = Allowance {
        balance: new_balance,
        expires: expires.unwrap_or(current.expires),
        description: current.description,
    };

    if new_balance.is_zero() {
        ALLOWANCES.remove(deps.storage, &spender_addr);
    } else {
        ALLOWANCES.save(deps.storage, &spender_addr, &updated)?;
    }

    Ok(Response::new()
        .add_attribute("method", "decrease_allowance")
        .add_attribute("spender", spender)
        .add_attribute("amount_removed", amount.to_string())
        .add_attribute("new_balance", new_balance.to_string()))
}

/// 设置津贴(覆盖现有设置,仅管理员)
fn execute_set_allowance(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    spender: String,
    amount: Uint128,
    expires: Expiration,
    description: Option<String>,
) -> Result<Response, ContractError> {
    // 验证管理员身份
    let state = CW1_SUBKEYS.load(deps.storage)?;
    if info.sender != state.admin {
        return Err(ContractError::UnauthorizedAdmin {});
    }

    // 验证子密钥地址
    let spender_addr = deps.api.addr_validate(&spender)?;

    // 不允许自己给自己授权
    if spender_addr == state.admin {
        return Err(ContractError::SelfDelegationNotAllowed {});
    }

    // 创建津贴
    let allowance = Allowance::new(amount, expires, description);

    if amount.is_zero() {
        ALLOWANCES.remove(deps.storage, &spender_addr);
    } else {
        ALLOWANCES.save(deps.storage, &spender_addr, &allowance)?;
    }

    Ok(Response::new()
        .add_attribute("method", "set_allowance")
        .add_attribute("spender", spender)
        .add_attribute("amount", amount.to_string()))
}

/// 撤销子密钥所有权限
fn execute_revoke_allowance(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    spender: String,
) -> Result<Response, ContractError> {
    // 验证管理员身份
    let state = CW1_SUBKEYS.load(deps.storage)?;
    if info.sender != state.admin {
        return Err(ContractError::UnauthorizedAdmin {});
    }

    let spender_addr = deps.api.addr_validate(&spender)?;

    // 删除津贴记录
    ALLOWANCES.remove(deps.storage, &spender_addr);

    Ok(Response::new()
        .add_attribute("method", "revoke_allowance")
        .add_attribute("spender", spender))
}

/// 转移管理员权限
fn execute_update_admin(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    admin: String,
) -> Result<Response, ContractError> {
    let state = CW1_SUBKEYS.load(deps.storage)?;
    if info.sender != state.admin {
        return Err(ContractError::UnauthorizedAdmin {});
    }

    let new_admin = deps.api.addr_validate(&admin)?;
    let new_state = Cw1SubkeysState { admin: new_admin };
    CW1_SUBKEYS.save(deps.storage, &new_state)?;

    Ok(Response::new()
        .add_attribute("method", "update_admin")
        .add_attribute("old_admin", info.sender)
        .add_attribute("new_admin", admin))
}

/// 查询入口
#[entry_point]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::Admin {} => to_json_binary(&query_admin(deps)?),
        QueryMsg::Allowance { spender } => {
            to_json_binary(&query_allowance(deps, env, spender)?)
        }
        QueryMsg::AllAllowances { start_after, limit } => {
            to_json_binary(&query_all_allowances(deps, env, start_after, limit)?)
        }
        QueryMsg::CanExecute { sender, msg } => {
            to_json_binary(&query_can_execute(deps, env, sender, msg)?)
        }
    }
}

/// 查询管理员
fn query_admin(deps: Deps) -> StdResult<AdminResponse> {
    let state = CW1_SUBKEYS.load(deps.storage)?;
    Ok(AdminResponse {
        admin: state.admin.to_string(),
    })
}

/// 查询指定子密钥的津贴
fn query_allowance(deps: Deps, env: Env, spender: String) -> StdResult<AllowanceResponse> {
    let spender_addr = deps.api.addr_validate(&spender)?;
    let allowance = ALLOWANCES.load(deps.storage, &spender_addr)?;
    Ok(AllowanceResponse { allowance })
}

/// 查询所有子密钥的津贴(分页)
fn query_all_allowances(
    deps: Deps,
    env: Env,
    start_after: Option<String>,
    limit: Option<u32>,
) -> StdResult<AllAllowancesResponse> {
    let limit = limit.unwrap_or(30).min(100) as usize;

    let start = start_after
        .as_ref()
        .map(|s| deps.api.addr_validate(s))
        .transpose()?;

    let allowances: StdResult<Vec<_>> = ALLOWANCES
        .range(deps.storage, start.as_ref(), None, Order::Ascending)
        .take(limit)
        .map(|item| {
            let (addr, allowance) = item?;
            Ok((addr.to_string(), allowance))
        })
        .collect();

    Ok(AllAllowancesResponse {
        allowances: allowances?,
    })
}

/// 检查发送者是否可执行指定消息
fn query_can_execute(
    deps: Deps,
    env: Env,
    sender: String,
    msg: CosmosMsg,
) -> StdResult<CanExecuteResponse> {
    let state = CW1_SUBKEYS.load(deps.storage)?;
    let sender_addr = deps.api.addr_validate(&sender)?;

    // 管理员始终可以执行
    if sender_addr == state.admin {
        return Ok(CanExecuteResponse { can_execute: true });
    }

    // 检查子密钥是否有有效津贴
    let allowance = match ALLOWANCES.may_load(deps.storage, &sender_addr)? {
        Some(a) => a,
        None => {
            return Ok(CanExecuteResponse {
                can_execute: false,
            })
        }
    };

    // 检查是否过期
    if allowance.expires.is_expired(env.block.height, env.block.time.nanos()) {
        return Ok(CanExecuteResponse {
            can_execute: false,
        });
    }

    // 检查是否有足够的余额来执行此消息
    let required = match &msg {
        CosmosMsg::Bank(BankMsg::Send { amount, .. }) => {
            amount.iter().fold(Uint128::zero(), |acc, c| acc + c.amount)
        }
        CosmosMsg::Wasm(WasmMsg::Execute { funds, .. }) => {
            funds.iter().fold(Uint128::zero(), |acc, c| acc + c.amount)
        }
        _ => Uint128::zero(),
    };

    Ok(CanExecuteResponse {
        can_execute: required <= allowance.balance,
    })
}

3.3.5 Lib 入口

// src/lib.rs
pub mod contract;
pub mod error;
pub mod msg;
pub mod state;

pub use crate::error::ContractError;
pub use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
pub use crate::state::{Allowance, Cw1SubkeysState, Expiration};

3.4 测试实现

// tests/integration.rs
use cosmwasm_std::{
    coin, coins, from_json,
    testing::{mock_dependencies, mock_env, mock_info},
    Addr, BankMsg, CosmosMsg, Uint128, WasmMsg, Binary,
};
use cw1_subkeys::{
    contract::{execute, instantiate, query},
    error::ContractError,
    msg::{
        AdminResponse, AllowanceResponse, AllAllowancesResponse,
        CanExecuteResponse, ExecuteMsg, InstantiateMsg, QueryMsg,
    },
    state::{Allowance, Expiration},
};

/// 辅助函数:创建合约实例
fn setup_contract(
    admin: Option<&str>,
) -> (
    cosmwasm_std::OwnedDeps<
        cosmwasm_std::MemoryStorage,
        cosmwasm_std::testing::MockApi,
        cosmwasm_std::testing::MockQuerier,
    >,
    cosmwasm_std::Env,
) {
    let mut deps = mock_dependencies();
    let env = mock_env();

    let msg = InstantiateMsg {
        admin: admin.map(|s| s.to_string()),
    };

    let info = mock_info("admin", &[]);
    instantiate(deps.as_mut(), env.clone(), info, msg).unwrap();

    (deps, env)
}

/// 辅助函数:设置津贴
fn setup_allowance(
    deps: &mut cosmwasm_std::OwnedDeps<
        cosmwasm_std::MemoryStorage,
        cosmwasm_std::testing::MockApi,
        cosmwasm_std::testing::MockQuerier,
    >,
    env: cosmwasm_std::Env,
    spender: &str,
    amount: Uint128,
    expires: Expiration,
) {
    let info = mock_info("admin", &[]);
    execute(
        deps.as_mut(),
        env,
        info,
        ExecuteMsg::SetAllowance {
            spender: spender.to_string(),
            amount,
            expires,
            description: Some("test allowance".to_string()),
        },
    )
    .unwrap();
}

#[test]
fn test_instantiate() {
    let (deps, _) = setup_contract(None);

    let admin_resp: AdminResponse = from_json(
        query(deps.as_ref(), mock_env(), QueryMsg::Admin {}).unwrap(),
    )
    .unwrap();
    assert_eq!(admin_resp.admin, "admin");
}

#[test]
fn test_instantiate_with_custom_admin() {
    let (deps, _) = setup_contract(Some("custom_admin"));

    let admin_resp: AdminResponse = from_json(
        query(deps.as_ref(), mock_env(), QueryMsg::Admin {}).unwrap(),
    )
    .unwrap();
    assert_eq!(admin_resp.admin, "custom_admin");
}

#[test]
fn test_set_allowance() {
    let (mut deps, env) = setup_contract(None);

    let info = mock_info("admin", &[]);
    execute(
        deps.as_mut(),
        env.clone(),
        info,
        ExecuteMsg::SetAllowance {
            spender: "subkey1".to_string(),
            amount: Uint128::new(1000),
            expires: Expiration::Never {},
            description: Some("gas relayer".to_string()),
        },
    )
    .unwrap();

    let resp: AllowanceResponse = from_json(
        query(
            deps.as_ref(),
            env,
            QueryMsg::Allowance {
                spender: "subkey1".to_string(),
            },
        )
        .unwrap(),
    )
    .unwrap();

    assert_eq!(resp.allowance.balance, Uint128::new(1000));
    assert_eq!(resp.allowance.expires, Expiration::Never {});
}

#[test]
fn test_set_allowance_by_non_admin() {
    let (mut deps, env) = setup_contract(None);

    let info = mock_info("attacker", &[]);
    let err = execute(
        deps.as_mut(),
        env,
        info,
        ExecuteMsg::SetAllowance {
            spender: "subkey1".to_string(),
            amount: Uint128::new(1000),
            expires: Expiration::Never {},
            description: None,
        },
    )
    .unwrap_err();

    assert_eq!(err, ContractError::UnauthorizedAdmin {});
}

#[test]
fn test_increase_allowance() {
    let (mut deps, env) = setup_contract(None);

    // 初始设置 500
    setup_allowance(&mut deps, env.clone(), "subkey1", Uint128::new(500), Expiration::Never {});

    // 增加 300
    let info = mock_info("admin", &[]);
    execute(
        deps.as_mut(),
        env.clone(),
        info,
        ExecuteMsg::IncreaseAllowance {
            spender: "subkey1".to_string(),
            amount: Uint128::new(300),
            expires: None,
        },
    )
    .unwrap();

    // 验证余额 = 800
    let resp: AllowanceResponse = from_json(
        query(
            deps.as_ref(),
            env,
            QueryMsg::Allowance {
                spender: "subkey1".to_string(),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(resp.allowance.balance, Uint128::new(800));
}

#[test]
fn test_decrease_allowance() {
    let (mut deps, env) = setup_contract(None);

    setup_allowance(&mut deps, env.clone(), "subkey1", Uint128::new(1000), Expiration::Never {});

    // 减少 200
    let info = mock_info("admin", &[]);
    execute(
        deps.as_mut(),
        env.clone(),
        info,
        ExecuteMsg::DecreaseAllowance {
            spender: "subkey1".to_string(),
            amount: Uint128::new(200),
            expires: None,
        },
    )
    .unwrap();

    let resp: AllowanceResponse = from_json(
        query(
            deps.as_ref(),
            env,
            QueryMsg::Allowance {
                spender: "subkey1".to_string(),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(resp.allowance.balance, Uint128::new(800));
}

#[test]
fn test_decrease_allowance_below_zero() {
    let (mut deps, env) = setup_contract(None);

    setup_allowance(&mut deps, env.clone(), "subkey1", Uint128::new(100), Expiration::Never {});

    // 尝试减少 200(超过余额)
    let info = mock_info("admin", &[]);
    execute(
        deps.as_mut(),
        env.clone(),
        info,
        ExecuteMsg::DecreaseAllowance {
            spender: "subkey1".to_string(),
            amount: Uint128::new(200),
            expires: None,
        },
    )
    .unwrap();

    // 津贴应降至 0 并被删除
    let resp: StdResult<AllowanceResponse> = from_json(
        query(
            deps.as_ref(),
            env,
            QueryMsg::Allowance {
                spender: "subkey1".to_string(),
            },
        ),
    );
    assert!(resp.is_err());
}

#[test]
fn test_execute_as_subkey() {
    let (mut deps, env) = setup_contract(None);

    setup_allowance(&mut deps, env.clone(), "subkey1", Uint128::new(5000), Expiration::Never {});

    let msgs = vec![BankMsg::Send {
        to_address: "recipient".to_string(),
        amount: coins(1000, "umsg"),
    }
    .into()];

    let info = mock_info("subkey1", &[]);
    let resp = execute(deps.as_mut(), env.clone(), info, ExecuteMsg::Execute { msgs }).unwrap();

    assert_eq!(resp.messages.len(), 1);

    // 验证额度已扣除
    let allowance_resp: AllowanceResponse = from_json(
        query(
            deps.as_ref(),
            env,
            QueryMsg::Allowance {
                spender: "subkey1".to_string(),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(allowance_resp.allowance.balance, Uint128::new(4000));
}

#[test]
fn test_execute_insufficient_allowance() {
    let (mut deps, env) = setup_contract(None);

    setup_allowance(&mut deps, env.clone(), "subkey1", Uint128::new(500), Expiration::Never {});

    let msgs = vec![BankMsg::Send {
        to_address: "recipient".to_string(),
        amount: coins(1000, "umsg"),
    }
    .into()];

    let info = mock_info("subkey1", &[]);
    let err = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs }).unwrap_err();

    assert_eq!(
        err,
        ContractError::InsufficientAllowance {
            available: Uint128::new(500),
            required: Uint128::new(1000),
        }
    );
}

#[test]
fn test_execute_expired_allowance() {
    let (mut deps, env) = setup_contract(None);

    // 设置一个已过期的津贴(区块高度 1 就过期)
    setup_allowance(
        &mut deps,
        env.clone(),
        "subkey1",
        Uint128::new(5000),
        Expiration::AtHeight(1),
    );

    let msgs = vec![BankMsg::Send {
        to_address: "recipient".to_string(),
        amount: coins(100, "umsg"),
    }
    .into()];

    let info = mock_info("subkey1", &[]);
    let err = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs }).unwrap_err();

    assert_eq!(err, ContractError::AllowanceExpired {});
}

#[test]
fn test_execute_as_admin_no_limit() {
    let (mut deps, env) = setup_contract(None);

    let msgs = vec![BankMsg::Send {
        to_address: "recipient".to_string(),
        amount: coins(1000000, "umsg"),
    }
    .into()];

    let info = mock_info("admin", &[]);
    let resp = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs }).unwrap();

    assert_eq!(resp.messages.len(), 1);
    // 管理员没有额度限制
}

#[test]
fn test_execute_unauthorized_sender() {
    let (mut deps, env) = setup_contract(None);

    let msgs = vec![BankMsg::Send {
        to_address: "recipient".to_string(),
        amount: coins(100, "umsg"),
    }
    .into()];

    let info = mock_info("unknown", &[]);
    let err = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs }).unwrap_err();

    assert_eq!(err, ContractError::UnauthorizedSubKey {});
}

#[test]
fn test_revoke_allowance() {
    let (mut deps, env) = setup_contract(None);

    setup_allowance(&mut deps, env.clone(), "subkey1", Uint128::new(5000), Expiration::Never {});

    // 撤销
    let info = mock_info("admin", &[]);
    execute(
        deps.as_mut(),
        env.clone(),
        info,
        ExecuteMsg::RevokeAllowance {
            spender: "subkey1".to_string(),
        },
    )
    .unwrap();

    // 验证被撤销
    let resp: StdResult<AllowanceResponse> = from_json(
        query(
            deps.as_ref(),
            env,
            QueryMsg::Allowance {
                spender: "subkey1".to_string(),
            },
        ),
    );
    assert!(resp.is_err());
}

#[test]
fn test_revoked_subkey_cannot_execute() {
    let (mut deps, env) = setup_contract(None);

    setup_allowance(&mut deps, env.clone(), "subkey1", Uint128::new(5000), Expiration::Never {});

    // 撤销
    let info = mock_info("admin", &[]);
    execute(
        deps.as_mut(),
        env.clone(),
        info,
        ExecuteMsg::RevokeAllowance {
            spender: "subkey1".to_string(),
        },
    )
    .unwrap();

    // 尝试执行
    let msgs = vec![BankMsg::Send {
        to_address: "recipient".to_string(),
        amount: coins(100, "umsg"),
    }
    .into()];

    let info = mock_info("subkey1", &[]);
    let err = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs }).unwrap_err();

    assert_eq!(err, ContractError::UnauthorizedSubKey {});
}

#[test]
fn test_update_admin() {
    let (mut deps, env) = setup_contract(None);

    let info = mock_info("admin", &[]);
    execute(
        deps.as_mut(),
        env.clone(),
        info,
        ExecuteMsg::UpdateAdmin {
            admin: "new_admin".to_string(),
        },
    )
    .unwrap();

    let resp: AdminResponse = from_json(
        query(deps.as_ref(), env, QueryMsg::Admin {}).unwrap(),
    )
    .unwrap();
    assert_eq!(resp.admin, "new_admin");
}

#[test]
fn test_self_delegation_not_allowed() {
    let (mut deps, env) = setup_contract(None);

    let info = mock_info("admin", &[]);
    let err = execute(
        deps.as_mut(),
        env,
        info,
        ExecuteMsg::SetAllowance {
            spender: "admin".to_string(),
            amount: Uint128::new(1000),
            expires: Expiration::Never {},
            description: None,
        },
    )
    .unwrap_err();

    assert_eq!(err, ContractError::SelfDelegationNotAllowed {});
}

#[test]
fn test_all_allowances_pagination() {
    let (mut deps, env) = setup_contract(None);

    // 添加多个子密钥
    for i in 0..10 {
        let spender = format!("subkey{}", i);
        setup_allowance(
            &mut deps,
            env.clone(),
            &spender,
            Uint128::new((i * 100) as u128),
            Expiration::Never {},
        );
    }

    // 查询全部
    let resp: AllAllowancesResponse = from_json(
        query(
            deps.as_ref(),
            env.clone(),
            QueryMsg::AllAllowances {
                start_after: None,
                limit: None,
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(resp.allowances.len(), 10);

    // 分页查询(每页 5 条)
    let resp_page1: AllAllowancesResponse = from_json(
        query(
            deps.as_ref(),
            env.clone(),
            QueryMsg::AllAllowances {
                start_after: None,
                limit: Some(5),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(resp_page1.allowances.len(), 5);
    assert_eq!(resp_page1.allowances[0].0, "subkey0");

    // 第二页
    let resp_page2: AllAllowancesResponse = from_json(
        query(
            deps.as_ref(),
            env,
            QueryMsg::AllAllowances {
                start_after: Some("subkey4".to_string()),
                limit: Some(5),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(resp_page2.allowances.len(), 5);
    assert_eq!(resp_page2.allowances[0].0, "subkey5");
}

#[test]
fn test_can_execute_query() {
    let (deps, env) = setup_contract(None);

    // 管理员始终可以执行
    let resp: CanExecuteResponse = from_json(
        query(
            deps.as_ref(),
            env.clone(),
            QueryMsg::CanExecute {
                sender: "admin".to_string(),
                msg: BankMsg::Send {
                    to_address: "anyone".to_string(),
                    amount: coins(1000000, "umsg"),
                }
                .into(),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert!(resp.can_execute);

    // 未授权的用户不能执行
    let resp: CanExecuteResponse = from_json(
        query(
            deps.as_ref(),
            env,
            QueryMsg::CanExecute {
                sender: "unknown".to_string(),
                msg: BankMsg::Send {
                    to_address: "anyone".to_string(),
                    amount: coins(100, "umsg"),
                }
                .into(),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert!(!resp.can_execute);
}

#[test]
fn test_multiple_messages_deduct_allowance_once() {
    let (mut deps, env) = setup_contract(None);

    setup_allowance(&mut deps, env.clone(), "subkey1", Uint128::new(3000), Expiration::Never {});

    // 一次性发送多条消息
    let msgs = vec![
        BankMsg::Send {
            to_address: "recipient1".to_string(),
            amount: coins(1000, "umsg"),
        }
        .into(),
        BankMsg::Send {
            to_address: "recipient2".to_string(),
            amount: coins(1500, "umsg"),
        }
        .into(),
    ];

    let info = mock_info("subkey1", &[]);
    let resp = execute(deps.as_mut(), env.clone(), info, ExecuteMsg::Execute { msgs }).unwrap();
    assert_eq!(resp.messages.len(), 2);

    // 验证总扣除 2500,剩余 500
    let allowance_resp: AllowanceResponse = from_json(
        query(
            deps.as_ref(),
            env,
            QueryMsg::Allowance {
                spender: "subkey1".to_string(),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(allowance_resp.allowance.balance, Uint128::new(500));
}

#[test]
fn test_expiration_at_time() {
    let (mut deps, mut env) = setup_contract(None);

    // 设置一个在未来时间过期的津贴
    setup_allowance(
        &mut deps,
        env.clone(),
        "subkey1",
        Uint128::new(1000),
        Expiration::AtTime(env.block.time.nanos() + 1_000_000_000), // 1秒后
    );

    // 当前时间,应该有效
    let msgs = vec![BankMsg::Send {
        to_address: "recipient".to_string(),
        amount: coins(100, "umsg"),
    }
    .into()];

    let info = mock_info("subkey1", &[]);
    execute(deps.as_mut(), env.clone(), info, ExecuteMsg::Execute { msgs }).unwrap();

    // 快进时间到过期后
    env.block.time = env.block.time.plus_seconds(2);

    let msgs2 = vec![BankMsg::Send {
        to_address: "recipient".to_string(),
        amount: coins(100, "umsg"),
    }
    .into()];

    let info = mock_info("subkey1", &[]);
    let err = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs2 }).unwrap_err();
    assert_eq!(err, ContractError::AllowanceExpired {});
}

#[test]
fn test_empty_messages() {
    let (mut deps, env) = setup_contract(None);

    let info = mock_info("admin", &[]);
    let err = execute(deps.as_mut(), env, info, ExecuteMsg::Execute { msgs: vec![] })
        .unwrap_err();
    assert_eq!(err, ContractError::EmptyMessages {});
}

#[test]
fn test_increase_allowance_overflow() {
    let (mut deps, env) = setup_contract(None);

    setup_allowance(
        &mut deps,
        env.clone(),
        "subkey1",
        Uint128::MAX,
        Expiration::Never {},
    );

    let info = mock_info("admin", &[]);
    let err = execute(
        deps.as_mut(),
        env,
        info,
        ExecuteMsg::IncreaseAllowance {
            spender: "subkey1".to_string(),
            amount: Uint128::new(1),
            expires: None,
        },
    )
    .unwrap_err();

    assert_eq!(err, ContractError::AllowanceOverflow {});
}

#[test]
fn test_invalid_zero_amount() {
    let (mut deps, env) = setup_contract(None);

    let info = mock_info("admin", &[]);
    let err = execute(
        deps.as_mut(),
        env,
        info,
        ExecuteMsg::IncreaseAllowance {
            spender: "subkey1".to_string(),
            amount: Uint128::zero(),
            expires: None,
        },
    )
    .unwrap_err();

    assert_eq!(err, ContractError::InvalidZeroAmount {});
}

#[test]
fn test_wasm_execute_funds_deducted() {
    let (mut deps, env) = setup_contract(None);

    setup_allowance(&mut deps, env.clone(), "subkey1", Uint128::new(2000), Expiration::Never {});

    let msgs = vec![WasmMsg::Execute {
        contract_addr: "contract".to_string(),
        msg: Binary::from(b"{\"action\":\"deposit\"}"),
        funds: coins(500, "umsg"),
    }
    .into()];

    let info = mock_info("subkey1", &[]);
    execute(deps.as_mut(), env.clone(), info, ExecuteMsg::Execute { msgs }).unwrap();

    let resp: AllowanceResponse = from_json(
        query(
            deps.as_ref(),
            env,
            QueryMsg::Allowance {
                spender: "subkey1".to_string(),
            },
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(resp.allowance.balance, Uint128::new(1500));
}

3.5 部署与交互

# 存储合约
RES=$(msg-chain-devkit tx wasm store artifacts/cw1_subkeys.wasm \
  --from admin \
  --gas auto \
  --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org \
  --output json)
CODE_ID=$(echo $RES | jq -r '.logs[0].events[] | select(.type == "store_code") | .attributes[] | select(.key == "code_id") | .value')

# 实例化
INIT='{"admin": "msg1admin..."}'
msg-chain-devkit tx wasm instantiate $CODE_ID "$INIT" \
  --from admin \
  --label "cw1_subkeys_payments" \
  --gas auto \
  --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1 \
  --node https://rpc.msgchain.org

# 存入资金
msg-chain-devkit tx bank send admin $CONTRACT_ADDR 5000000umsg \
  --chain-id msg-chain-1

# 设置子密钥津贴
SET_ALLOWANCE='{"set_allowance":{"spender":"msg1relayer...","amount":"1000000","expires":{"never":{}},"description":"gas relayer"}}'
msg-chain-devkit tx wasm execute $CONTRACT_ADDR "$SET_ALLOWANCE" \
  --from admin \
  --gas auto \
  --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1

# 子密钥执行转账
EXECUTE='{"execute":{"msgs":[{"bank":{"send":{"to_address":"msg1recipient...","amount":[{"denom":"umsg","amount":"500"}]}}}]}}'
msg-chain-devkit tx wasm execute $CONTRACT_ADDR "$EXECUTE" \
  --from relayer \
  --gas auto \
  --gas-prices 1000000000attoMSG \
  --chain-id msg-chain-1

# 查询
msg-chain-devkit query wasm contract-state smart $CONTRACT_ADDR '{"allowance":{"spender":"msg1relayer..."}}'
msg-chain-devkit query wasm contract-state smart $CONTRACT_ADDR '{"all_allowances":{"start_after":null,"limit":10}}'
msg-chain-devkit query wasm contract-state smart $CONTRACT_ADDR '{"can_execute":{"sender":"msg1relayer...","msg":{"bank":{"send":{"to_address":"msg1recipient...","amount":[{"denom":"umsg","amount":"500"}]}}}}}'

3.6 cw1_subkeys 高级功能

3.6.1 定期重置津贴 (Recurring Allowance)

/// 周期性津贴
pub struct RecurringAllowance {
    /// 每周期额度
    pub period_amount: Uint128,
    /// 周期长度(秒)
    pub period_seconds: u64,
    /// 当前周期已使用
    pub spent_this_period: Uint128,
    /// 周期开始时间
    pub period_start: u64,
    /// 总余额上限
    pub max_balance: Uint128,
    /// 过期时间
    pub expires: Expiration,
}

impl RecurringAllowance {
    /// 重置周期
    pub fn maybe_reset(&mut self, current_time: u64) {
        let elapsed = current_time - self.period_start;
        if elapsed >= self.period_seconds {
            // 重置使用量(但不超过最大余额)
            self.spent_this_period = Uint128::zero();
            self.period_start = current_time;
        }
    }

    /// 获取当前可用余额
    pub fn available(&self) -> Uint128 {
        self.period_amount - self.spent_this_period
    }
}

3.6.2 多资产津贴

/// 多资产津贴(按 denom 分别限制)
pub struct MultiAssetAllowance {
    /// 资产限制: denom -> 限额
    pub denom_limits: Vec<DenomLimit>,
    /// 过期时间
    pub expires: Expiration,
}

pub struct DenomLimit {
    pub denom: String,
    pub max_amount: Uint128,
    pub spent: Uint128,
}

4. 集成场景

4.1 Gas Station Relayer

Gas Station 是最典型的 CW1 应用场景。用户将 MSG 代币存入合约,授权 Relayer 从中支付 Gas 费用。

4.1.1 架构图

┌─────────────────┐     ┌──────────────────┐     ┌──────────────┐
│  终端用户         │     │  CW1 Subkeys      │     │ Relayer 服务  │
│ (无 MSG 代币)    │────▶│  合约             │────▶│ (运行中继器)   │
│                  │     │  - 预存 Gas 费用  │     │              │
│ 签署交易 →       │     │  - 授权 Relayer   │     │ 提交交易 →    │
│ 发送到 Relayer   │     │  - 限额 1000 MSG  │     │ MSG Chain    │
└─────────────────┘     └──────────────────┘     └──────────────┘

4.1.2 Relayer 合约设置

# 1. 管理员部署合约
msg-chain-devkit tx wasm instantiate $CODE_ID \
  '{"admin":"msg1user..."}' \
  --from user --label "cw1_gas_station"

# 2. 存入 Gas 费用
msg-chain-devkit tx bank send user $CONTRACT_ADDR 5000000umsg

# 3. 授权 Relayer(每周限额 1000 MSG)
SET_ALLOWANCE='{
  "set_allowance":{
    "spender":"msg1relayer_service...",
    "amount":"1000000",
    "expires":{"at_time":"18000000000000000000"},
    "description":"weekly gas budget"
  }
}'
msg-chain-devkit tx wasm execute $CONTRACT_ADDR "$SET_ALLOWANCE" --from user

4.1.3 Relayer 服务核心逻辑 (Rust)

// relayer/src/main.rs
use cosmwasm_std::{BankMsg, Coin, CosmosMsg};
use msg_chain_sdk::{MsgChainClient, TxBuilder, Wallet};

/// Relayer 服务:代表用户提交交易并扣除 Gas 费用
pub struct GasRelayer {
    /// MSG Chain 客户端
    client: MsgChainClient,
    /// Relayer 钱包
    wallet: Wallet,
    /// 每个用户的 CW1 合约地址
    user_contracts: HashMap<String, String>,
}

impl GasRelayer {
    /// 提交用户交易并通过 CW1 合约扣除 Gas
    pub async fn relay_transaction(
        &self,
        user_address: &str,
        msgs: Vec<CosmosMsg>,
        gas_limit: u64,
    ) -> Result<TxResponse, RelayerError> {
        // 1. 获取用户的 CW1 合约
        let contract = self.user_contracts.get(user_address)
            .ok_or(RelayerError::NoContractFound)?;

        // 2. 计算 Gas 成本
        let gas_cost = self.estimate_gas_cost(&msgs, gas_limit);

        // 3. 构建 Gas 转账消息
        let gas_msg: CosmosMsg = BankMsg::Send {
            to_address: self.wallet.address(),
            amount: vec![Coin {
                denom: "umsg".to_string(),
                amount: gas_cost,
            }],
        }.into();

        // 4. 合并用户消息和 Gas 费用消息
        let mut all_msgs = msgs;
        all_msgs.push(gas_msg);

        // 5. 构建 CW1 execute 消息
        let execute_msg = serde_json::json!({
            "execute": { "msgs": all_msgs }
        });

        // 6. 作为 Relayer 提交交易
        let tx = TxBuilder::new()
            .add_execute_contract_msg(contract, &execute_msg, vec![])
            .build(&self.wallet)?;

        let resp = self.client.broadcast_tx(tx).await?;
        Ok(resp)
    }

    /// 估算 Gas 成本
    fn estimate_gas_cost(&self, msgs: &[CosmosMsg], gas_limit: u64) -> Uint128 {
        // 简化估算: gas_limit * gas_price
        Uint128::from(gas_limit) * Uint128::from(25_000_000u128) / Uint128::from(1_000_000u128)
    }
}

4.1.4 Relayer 安全措施

/// 防止滥用:限制每个用户的 Gas 使用频率
pub struct RateLimiter {
    /// 每分钟最多执行的交易数
    max_tx_per_minute: u32,
    /// 每次交易最大 Gas 费用
    max_gas_per_tx: Uint128,
    /// 追踪窗口
    windows: HashMap<String, Vec<u64>>,
}

impl RateLimiter {
    pub fn check_rate_limit(&mut self, user: &str, current_time: u64) -> Result<(), RelayerError> {
        // 清理超过 60 秒的记录
        let window = self.windows.entry(user.to_string()).or_default();
        window.retain(|t| current_time - *t < 60);

        // 检查频率
        if window.len() >= self.max_tx_per_minute as usize {
            return Err(RelayerError::RateLimitExceeded);
        }

        window.push(current_time);
        Ok(())
    }
}

4.2 订阅支付

# 1. 用户部署合约并存入 12 个月的订阅费
msg-chain-devkit tx bank send user $CONTRACT_ADDR 12000000umsg

# 2. 授权 SaaS 平台每月扣除 1000 MSG
SET_ALLOWANCE='{
  "set_allowance":{
    "spender":"msg1saas_platform...",
    "amount":"12000000",
    "expires":{"never":{}},
    "description":"monthly subscription"
  }
}'
msg-chain-devkit tx wasm execute $CONTRACT_ADDR "$SET_ALLOWANCE" --from user

# 3. 平台每月调用扣除
EXECUTE='{"execute":{"msgs":[{"bank":{"send":{"to_address":"msg1saas_platform...","amount":[{"denom":"umsg","amount":"1000000"}]}}}]}}'
msg-chain-devkit tx wasm execute $CONTRACT_ADDR "$EXECUTE" --from saas_platform

订阅管理合约

/// 订阅管理合约
pub struct SubscriptionManager {
    /// 每个用户当前的订阅计划
    pub subscriptions: Map<&Addr, Subscription>,
    /// CW1 合约地址
    pub cw1_contract: Addr,
}

pub struct Subscription {
    pub plan: String,
    pub amount: Uint128,
    pub frequency: u64,  // 秒
    pub next_billing: u64,
}

impl SubscriptionManager {
    /// 执行批量扣款
    pub fn process_billing(&self, deps: DepsMut, env: Env) -> Result<Vec<CosmosMsg>, ContractError> {
        let current_time = env.block.time.nanos();
        let mut billings = vec![];

        // 遍历所有待扣款的订阅
        let subscriptions: Vec<_> = self.subscriptions
            .range(deps.storage, None, None, Order::Ascending)
            .filter_map(|item| {
                let (addr, sub) = item.ok()?;
                if current_time >= sub.next_billing {
                    Some((addr, sub))
                } else {
                    None
                }
            })
            .collect();

        for (user, sub) in subscriptions {
            let msg: CosmosMsg = BankMsg::Send {
                to_address: self.cw1_contract.to_string(),
                amount: vec![Coin {
                    denom: "umsg".to_string(),
                    amount: sub.amount,
                }],
            }.into();

            billings.push(msg);
        }

        Ok(billings)
    }
}

4.3 委托交易

/// 交易机器人授权
pub struct TradingBotAllowance {
    /// 每笔交易最大金额
    pub max_per_trade: Uint128,
    /// 日交易总额限制
    pub daily_limit: Uint128,
    /// 今日已交易总额
    pub today_spent: Uint128,
    /// 允许的交易对
    pub allowed_pairs: Vec<String>,
    /// 上次重置日期
    pub last_reset_day: u64,
}

impl TradingBotAllowance {
    /// 验证交易是否允许执行
    pub fn validate_trade(
        &mut self,
        amount: Uint128,
        pair: &str,
        current_day: u64,
    ) -> Result<(), TradingError> {
        // 日限额重置
        if current_day != self.last_reset_day {
            self.today_spent = Uint128::zero();
            self.last_reset_day = current_day;
        }

        // 检查交易对
        if !self.allowed_pairs.contains(&pair.to_string()) {
            return Err(TradingError::PairNotAllowed);
        }

        // 检查单笔限额
        if amount > self.max_per_trade {
            return Err(TradingError::ExceedsMaxPerTrade);
        }

        // 检查日限额
        let new_total = self.today_spent + amount;
        if new_total > self.daily_limit {
            return Err(TradingError::ExceedsDailyLimit);
        }

        self.today_spent = new_total;
        Ok(())
    }
}

4.4 TypeScript 集成示例

// scripts/cw1_interact.ts
import { MsgChainClient, Wallet, Msg } from "@msg-chain/sdk";

async function setupCw1Subkeys() {
  const client = new MsgChainClient({
    rpcUrl: "https://rpc.msgchain.org",
    chainId: "msg-chain-1",
  });

  const wallet = await Wallet.fromMnemonic("your mnemonic here...");

  // 1. 查询合约
  const allowance = await client.queryContractSmart(
    "msg1contract...",
    { allowance: { spender: "msg1relayer..." } }
  );
  console.log("Allowance:", allowance);

  // 2. 设置津贴
  const setMsg = {
    set_allowance: {
      spender: "msg1relayer...",
      amount: "1000000",
      expires: { never: {} },
      description: "gas relayer allowance",
    },
  };

  const tx = await client.execute(wallet, "msg1contract...", setMsg, []);
  console.log("TX:", tx.transactionHash);

  // 3. 子密钥执行转账
  const executeMsg = {
    execute: {
      msgs: [
        {
          bank: {
            send: {
              to_address: "msg1recipient...",
              amount: [{ denom: "umsg", amount: "500" }],
            },
          },
        },
      ],
    },
  };

  const subKeyWallet = await Wallet.fromMnemonic("relayer mnemonic...");
  const execTx = await client.execute(subKeyWallet, "msg1contract...", executeMsg, []);
  console.log("Execute TX:", execTx.transactionHash);

  // 4. 查询多个授权
  const allAllowances = await client.queryContractSmart(
    "msg1contract...",
    { all_allowances: { start_after: null, limit: 20 } }
  );
  console.log("All allowances:", allAllowances);
}

4.5 WebSocket 事件监听

// 监听 CW1 合约事件
const ws = new WebSocket("wss://rpc.msgchain.org/websocket");

ws.onopen = () => {
  // 订阅 CW1 合约的执行事件
  const query = JSON.stringify({
    jsonrpc: "2.0",
    method: "subscribe",
    params: ["tm.event='Tx' AND execute._contract_address='msg1contract...'"],
    id: 1,
  });
  ws.send(query);
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.result && data.result.events) {
    const events = data.result.events;
    console.log("Execute event:", {
      sender: events["execute.sender"],
      method: events["execute.method"],
      spent: events["execute.spent"],
      remaining: events["execute.remaining"],
    });
  }
};

5. 安全考虑

5.1 Allowance OverFlow 防护

/// 安全的加法操作
pub fn safe_add(a: Uint128, b: Uint128) -> Result<Uint128, ContractError> {
    a.checked_add(b).map_err(|_| ContractError::AllowanceOverflow {})
}

/// 安全的减法操作(不允许负数)
pub fn safe_sub(a: Uint128, b: Uint128) -> Result<Uint128, ContractError> {
    a.checked_sub(b).map_err(|_| ContractError::InsufficientAllowance {
        available: a,
        required: b,
    })
}

风险分析:

攻击场景:管理员增加无限额度
- 初始额度: 2^128 - 1 (Uint128::MAX)
- 增加额度: 1
- 结果: 溢出 → 额度变为 0

防护:
- 始终使用 checked_add / checked_sub
- 永远不要使用 + 或 - 运算符
- 在 increase_allowance 中添加溢出检查

5.2 Admin Key 管理

/// 多签管理员
pub struct MultiSigAdmin {
    /// 签名者列表
    pub signers: Vec<Addr>,
    /// 所需签名数
    pub required: u64,
}

impl MultiSigAdmin {
    /// 验证签名数量是否足够
    pub fn verify_signatures(
        &self,
        msg: &[u8],
        signatures: &[Vec<u8>],
    ) -> Result<(), ContractError> {
        if signatures.len() < self.required as usize {
            return Err(ContractError::InsufficientSignatures {
                required: self.required,
                provided: signatures.len() as u64,
            });
        }

        let mut valid_count = 0u64;
        for sig in signatures {
            for signer in &self.signers {
                if verify_signature(signer, msg, sig) {
                    valid_count += 1;
                    break;
                }
            }
        }

        if valid_count < self.required {
            return Err(ContractError::InsufficientSignatures {
                required: self.required,
                provided: valid_count,
            });
        }

        Ok(())
    }
}

管理员安全最佳实践:

1. 使用多签地址作为 Admin
   - 推荐 2/3 或 3/5 多签
   - 避免单点故障

2. 分阶段管理
   部署阶段: 部署者地址(临时)
   初始化后: 转移给多签地址
   紧急情况: 预先设置备用管理员

3. 操作审计
   所有管理员操作记录 on-chain event
   定期审查操作日志

5.3 津贴撤销与紧急停止

/// 紧急停止机制
pub struct EmergencyStop {
    /// 是否已触发紧急停止
    pub is_paused: bool,
    /// 可以触发暂停的地址
    pub pausers: Vec<Addr>,
    /// 紧急操作记录
    pub emergency_log: Vec<EmergencyAction>,
}

pub struct EmergencyAction {
    pub action_type: String,
    pub executor: Addr,
    pub timestamp: u64,
    pub reason: String,
}

impl EmergencyStop {
    /// 触发紧急暂停
    pub fn pause(&mut self, caller: &Addr, reason: String) -> Result<(), ContractError> {
        if !self.pausers.contains(caller) {
            return Err(ContractError::UnauthorizedPauser {});
        }

        self.is_paused = true;
        self.emergency_log.push(EmergencyAction {
            action_type: "PAUSE".to_string(),
            executor: caller.clone(),
            timestamp: env.block.time.nanos(),
            reason,
        });

        Ok(())
    }

    /// 批量撤销所有子密钥
    pub fn revoke_all(deps: DepsMut, admin: &Addr) -> Result<Response, ContractError> {
        // 获取所有子密钥
        let subkeys: Vec<Addr> = ALLOWANCES
            .keys(deps.storage, None, None, Order::Ascending)
            .collect::<StdResult<Vec<_>>>()?;

        // 批量删除
        for subkey in &subkeys {
            ALLOWANCES.remove(deps.storage, subkey);
        }

        Ok(Response::new()
            .add_attribute("method", "revoke_all")
            .add_attribute("count", subkeys.len().to_string())
            .add_attribute("admin", admin))
    }
}

5.4 重放攻击防护

/// 防止重放攻击:使用 nonce 机制
pub struct ReplayProtection {
    /// 每个子密钥的 nonce 计数器
    pub nonces: Map<&Addr, u64>,
}

impl ReplayProtection {
    /// 验证并递增 nonce
    pub fn verify_and_increment(
        &mut self,
        deps: DepsMut,
        sender: &Addr,
        expected_nonce: u64,
    ) -> Result<(), ContractError> {
        let current_nonce = self.nonces
            .may_load(deps.storage, sender)?
            .unwrap_or(0);

        if expected_nonce != current_nonce {
            return Err(ContractError::InvalidNonce {
                expected: current_nonce,
                provided: expected_nonce,
            });
        }

        self.nonces.save(deps.storage, sender, &(current_nonce + 1))?;
        Ok(())
    }
}

5.5 过期时间安全

/// 安全的过期时间检查
pub fn validate_expiration(expires: &Expiration, env: &Env) -> Result<(), ContractError> {
    match expires {
        Expiration::AtHeight(height) => {
            // 不允许设置已过去的高度
            if *height <= env.block.height {
                return Err(ContractError::InvalidExpiration {
                    reason: "Expiration height is in the past".to_string(),
                });
            }
            // 最大过期高度限制(防止无限期锁定问题)
            let max_height = env.block.height + 10_000_000; // ~2年
            if *height > max_height {
                return Err(ContractError::InvalidExpiration {
                    reason: "Expiration height too far in the future".to_string(),
                });
            }
        }
        Expiration::AtTime(time) => {
            if *time <= env.block.time.nanos() {
                return Err(ContractError::InvalidExpiration {
                    reason: "Expiration time is in the past".to_string(),
                });
            }
        }
        Expiration::Never {} => {
            // 永不过期需要管理员确认
            // 建议在业务逻辑层做限制
        }
    }
    Ok(())
}

/// 过期时间的最大建议值
pub const MAX_EXPIRATION_HEIGHT_DELTA: u64 = 10_000_000; // ~2年
pub const MAX_EXPIRATION_TIME_DELTA: u64 = 63_072_000_000_000_000; // ~2年(纳秒)

5.6 已知攻击向量与防护

攻击向量 描述 防护措施
额度耗尽 子密钥一次性转走所有资金 设置单笔交易限额、日限额
重放攻击 重复提交已签名的交易 Nonce 机制、有效期检查
Admin 私钥泄露 攻击者获得管理员权限 多签管理、硬件钱包、定期轮换
过期时间绕过 使用已过期的授权 在每次 execute 中检查过期时间
整数溢出 通过溢出操纵额度 Rust 的 checked_add/sub、safe math
前端跑 抢先交易窃取 滑点保护、commit-reveal 方案
假地址攻击 使用相似的地址进行欺骗 合约内部始终验证解析后的 Addr
Gas 耗尽攻击 提交大量小交易耗尽 Relayer Gas 最小交易金额、频率限制
授权链攻击 A 授权 B, B 授权 C, C 盗取资金 禁止链式授权、仅允许直接授权

5.7 审计清单

□ 所有金额计算使用 checked_add / checked_sub
□ 所有地址输入使用 addr_validate 验证
□ 管理员操作有事件日志
□ 过期时间不能在过去
□ 不能自己授权自己
□ 零金额操作被禁止
□ 空消息列表被禁止
□ 子密钥不能授权其他子密钥
□ 合约冻结后不能执行任何操作
□ 分页查询有最大限制
□ Nonce 防止重放
□ 管理权限转移有两步确认(可选)
□ 合约资金可回收(紧急提现功能)

5.8 紧急情况处理流程

1. 暂停合约
   execute(CONTRACT, { freeze: {} }, --from admin)

2. 如果 Admin 已经失窃,使用备用管理员或治理提案

3. 撤销所有子密钥
   execute(CONTRACT, { revoke_all: {} }, --from admin)

4. 提取剩余资金到安全地址
   execute(CONTRACT, {
     execute: {
       msgs: [{
         bank: {
           send: {
             to_address: "msg1safe_wallet...",
             amount: [{ denom: "umsg", amount: "999999999" }]
           }
         }
       }]
     }
   }, --from admin)

5. 部署新合约并重新授权

6. 附录

6.1 完整文件结构

cw1-subkeys/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   ├── contract.rs      # 合约入口 + execute/query 逻辑
│   ├── msg.rs           # 消息类型定义
│   ├── state.rs         # 状态存储结构
│   └── error.rs         # 错误类型
├── tests/
│   └── integration.rs   # 集成测试
├── schema/
│   ├── instantiate_msg.json
│   ├── execute_msg.json
│   └── query_msg.json
├── artifacts/           # 编译产物
│   ├── cw1_subkeys.wasm
│   └── cw1_subkeys-aarch64.wasm
└── scripts/
    ├── deploy.sh
    ├── interact.sh
    └── testnet_setup.sh

6.2 快速启动脚本

#!/bin/bash
# scripts/deploy.sh
# MSG Chain CW1 Subkeys 部署脚本

set -e

CHAIN_ID="msg-chain-1"
RPC_URL="https://rpc.msgchain.org"
GAS_PRICES="1000000000attoMSG"
FROM="admin"
CONTRACT_WASM="artifacts/cw1_subkeys.wasm"

echo "=== CW1 Subkeys 部署脚本 ==="
echo "Chain: $CHAIN_ID"
echo ""

# 1. 编译
echo ">> 编译合约..."
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
mkdir -p artifacts
wasm-opt -Os target/wasm32-unknown-unknown/release/cw1_subkeys.wasm -o $CONTRACT_WASM
echo "  编译完成"

# 2. 存储代码
echo ">> 存储合约代码..."
STORE_RES=$(msg-chain-devkit tx wasm store $CONTRACT_WASM \
  --from $FROM \
  --gas auto \
  --gas-prices $GAS_PRICES \
  --chain-id $CHAIN_ID \
  --node $RPC_URL \
  --output json)
CODE_ID=$(echo $STORE_RES | jq -r '.logs[0].events[] | select(.type == "store_code") | .attributes[] | select(.key == "code_id") | .value')
echo "  Code ID: $CODE_ID"

# 3. 实例化
echo ">> 实例化合约..."
INIT_MSG='{"admin": null}'
INST_RES=$(msg-chain-devkit tx wasm instantiate $CODE_ID "$INIT_MSG" \
  --from $FROM \
  --label "cw1_subkeys_$(date +%s)" \
  --gas auto \
  --gas-prices $GAS_PRICES \
  --chain-id $CHAIN_ID \
  --node $RPC_URL \
  --output json)
CONTRACT_ADDR=$(echo $INST_RES | jq -r '.logs[0].events[] | select(.type == "instantiate") | .attributes[] | select(.key == "_contract_address") | .value')
echo "  Contract: $CONTRACT_ADDR"

# 4. 存入初始资金
echo ">> 存入初始资金..."
msg-chain-devkit tx bank send $FROM $CONTRACT_ADDR 1000000000umsg \
  --chain-id $CHAIN_ID \
  --node $RPC_URL

echo ""
echo "=== 部署完成 ==="
echo "Code ID:     $CODE_ID"
echo "Contract:    $CONTRACT_ADDR"
echo "Explorer:    https://explorer.msgchain.org/contracts/$CONTRACT_ADDR"

6.3 测试网快速测试

# 获取测试网代币
curl -X POST https://faucet.msgchain.org/claim \
  -H "Content-Type: application/json" \
  -d '{"address": "msg1your_test_address..."}'

# 部署
bash scripts/deploy.sh

# 设置授权
msg-chain-devkit tx wasm execute $CONTRACT_ADDR \
  '{"set_allowance":{"spender":"msg1subkey...","amount":"1000000","expires":{"never":{}},"description":"test"}}' \
  --from admin --gas auto --gas-prices 1000000000attoMSG --chain-id msg-chain-1

# 执行
msg-chain-devkit tx wasm execute $CONTRACT_ADDR \
  '{"execute":{"msgs":[{"bank":{"send":{"to_address":"msg1recipient...","amount":[{"denom":"umsg","amount":"500"}]}}}]}}' \
  --from subkey --gas auto --gas-prices 1000000000attoMSG --chain-id msg-chain-1

# 验证
msg-chain-devkit query wasm contract-state smart $CONTRACT_ADDR \
  '{"allowance":{"spender":"msg1subkey..."}}'

6.4 主网部署检查清单

□ 合约代码已审计(推荐第三方审计公司)
□ 所有测试通过(cargo test)
□ wasm 文件已优化(wasm-opt 压缩)
□ Schema 已生成(cargo schema)
□ 管理员地址设置为多签地址
□ 初始资金已存入
□ 子密钥权限最小化原则
□ 过期时间合理设置
□ 已配置事件监控和告警
□ 紧急恢复计划已准备
□ 私钥管理方案已落实(硬件钱包/MPC)
□ 文档已更新

6.5 参考资源

6.6 版本历史

版本 日期 变更内容
1.0.0 2026-07-06 初始版本,涵盖 cw1_whitelist 和 cw1_subkeys 完整实现

免责声明: 本指南仅供学习和参考。在生产环境中使用前,请确保合约代码经过专业安全审计。智能合约一旦部署即不可篡改,任何漏洞都可能导致资金损失。请谨慎操作,风险自负。


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