CW1 (Subkeys/Allowance) 合约实现指南
数据来源:MSG Chain 代码库核实
主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。
目标链: MSG Chain (
msg-chain-1)
Bech32 前缀:msg
标准版本: CosmWasm CW1 (账户委托/津贴)
目录
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 | 复杂的授权管理 |
选择建议:
- 如果只需要一个简单的 Gas Station,
cw1_whitelist足够 - 如果需要 多级权限管理 + 独立限额,选择
cw1_subkeys
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 实现。它的核心规则:
- Admin 可以随时添加/移除白名单地址
- 白名单中的地址 可以从合约中转出任意金额的代币(但仅限转账到白名单中定义的目标地址)
- 非白名单地址 没有任何权限
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 的增强版。它支持:
- 每个子密钥独立的津贴(Allowance):包括金额限制和过期时间
- 动态调整津贴:管理员可以增加或减少特定子密钥的限额
- 支出跟踪:追踪每个子密钥的已使用额度
- 过期管理:津贴可以设置过期时间,到期自动失效
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 参考资源
- CosmWasm 官方文档: https://docs.cosmwasm.com
- CW1 标准规范: https://github.com/CosmWasm/cw-plus/tree/main/packages/cw1
- cw1-whitelist 参考实现: https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw1-whitelist
- cw1-subkeys 参考实现: https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw1-subkeys
- MSG Chain 文档: https://docs.msgchain.org
- MSG Chain 水龙头: https://faucet.msgchain.org
- MSG Chain 浏览器: https://explorer.msgchain.org
- CosmWasm 安全最佳实践: https://docs.cosmwasm.com/docs/security
- Rust Uint128 文档: https://docs.rs/cosmwasm-std/latest/cosmwasm_std/struct.Uint128.html
6.6 版本历史
| 版本 | 日期 | 变更内容 |
|---|---|---|
| 1.0.0 | 2026-07-06 | 初始版本,涵盖 cw1_whitelist 和 cw1_subkeys 完整实现 |
免责声明: 本指南仅供学习和参考。在生产环境中使用前,请确保合约代码经过专业安全审计。智能合约一旦部署即不可篡改,任何漏洞都可能导致资金损失。请谨慎操作,风险自负。
本文档基于 MSG Chain 代码库核实的技术事实。
白皮书系统: https://msgchain.org/whitepaper/
