dApp Docs/多签钱包合约实现
Development reference. Not independently verified for production.

MSG Chain 多签钱包合约实现指南

主网状态: No-Go

目录

  1. 多签概述
  2. 合约完整实现 — cw3_fixed_multisig
  3. 合约完整实现 — cw3_flex_multisig
  4. 多签操作流程
  5. 前端组件
  6. 安全最佳实践
  7. 集成场景
  8. 附录

1. 多签概述

1.1 什么是多重签名钱包

多重签名(Multi-Signature, Multi-Sig)是一种需要多个私钥持有者共同签署才能执行交易的钱包机制。与传统的单签名钱包(一个私钥即可控制所有资产)不同,多签钱包采用 t-of-n 模型,即 n 个授权签名者中,至少需要 t 个签名才能执行操作。

核心公式:

threshold (t) ≤ total_weight (n) 且 threshold ≥ 1

例如,3/5 多签表示 5 个签名者中至少需要 3 人同意才能执行任何操作。

1.2 CW3 标准概述

CosmWasm 的 CW3 标准定义了多签合约的通用接口。它建立在 CW1(代理合约)基础之上,扩展了投票和治理功能。CW3 标准包含以下核心消息类型:

消息 描述
Propose{title, description, msgs, earliest} 创建提案
Vote{proposal_id, vote} 对提案投票(Yes/No/Veto/Abstain)
Execute{proposal_id} 执行已通过的提案
Close{proposal_id} 关闭已过期或失败的提案
ListVoters{} 列出所有投票者
ListProposals{} 列出所有提案
ReverseProposals{} 反向列出提案
Voter{address} 查询某个投票者信息
Proposal{proposal_id} 查询提案详情

1.3 Fixed vs Flexible Multisig

CW3 标准提供了两种主要实现:

特性 cw3_fixed_multisig cw3_flex_multisig
签名者集合 实例化时固定,不可更改 可通过提案动态增删
阈值 实例化时固定 可通过提案更改
适用场景 团队金库、DAO 财库 需要成员变动的组织
实现复杂度 低 中
Gas 消耗 低 中等

1.4 MSG Chain 上的多签应用场景

DAO 财库管理

dao_governance_v1 合约使用 CW3 作为财库合约,确保治理提案通过后,财库资金只能经由多签投票执行。这实现了"治理投票 + 多签执行"的双层安全架构。

DAO 治理提案 → 投票通过 → 多签财库执行 → 资金转移

团队资金管理

项目方使用多签钱包管理:

高价值操作保护

Agent MPC 钱包对比

特性 CW3 多签 Agent MPC 钱包
签名方式 链上投票聚合 off-chain MPC 计算
签名者数量 3-10 较优 2-16
交易类型 任意 CosmWasm 消息 任意 CosmWasm 消息
隐私性 公开投票记录 签名过程不公开
Gas 成本 多笔投票交易 单笔交易
灵活性 投票规则可编程 策略引擎驱动

2. 合约完整实现 — cw3_fixed_multisig

2.1 存储布局

use cosmwasm_std::{
    Addr, Binary, BlockInfo, CosmosMsg, Decimal, Empty, QuerierWrapper,
    Storage, Timestamp, Uint128,
};
use cw_storage_plus::{Item, Map, SnapshotMap, Strategy};
use cw_utils::{Duration, Expiration, Threshold};

// 合约配置
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Config {
    pub threshold: Threshold,
    pub max_voting_period: Duration,
    pub total_weight: Uint128,
}

// 投票者信息
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Voter {
    pub addr: String,
    pub weight: Uint128,
}

// 提案状态
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum ProposalStatus {
    Pending,
    Open,
    Passed,
    Rejected,
    Executed,
    Closed,
    Expired,
}

// 投票类型
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub enum VoteType {
    Yes,
    No,
    Veto,
    Abstain,
}

// 提案结构
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Proposal {
    pub id: u64,
    pub title: String,
    pub description: String,
    pub msgs: Vec<CosmosMsg<Empty>>,
    pub status: ProposalStatus,
    pub expires: Expiration,
    pub threshold: Threshold,
    pub total_weight: Uint128,
    pub yes_weight: Uint128,
    pub no_weight: Uint128,
    pub veto_weight: Uint128,
    pub abstain_weight: Uint128,
    pub proposer: Addr,
    pub created: Timestamp,
    pub executed_at: Option<Timestamp>,
}

// 投票记录
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Vote {
    pub proposal_id: u64,
    pub voter: Addr,
    pub vote: VoteType,
    pub weight: Uint128,
    pub voted_at: Timestamp,
}

// 存储 Key
pub const CONFIG: Item<Config> = Item::new("config");
pub const VOTERS: Map<&Addr, Voter> = Map::new("voters");
pub const PROPOSAL_COUNT: Item<u64> = Item::new("proposal_count");
pub const PROPOSALS: Map<u64, Proposal> = Map::new("proposals");
pub const VOTES: Map<(u64, &Addr), Vote> = Map::new("votes");

2.2 完整合约实现

use cosmwasm_std::{
    attr, entry_point, to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut,
    Empty, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128,
};
use cw2::set_contract_version;
use cw_utils::{Expiration, Threshold, ThresholdResponse};

const CONTRACT_NAME: &str = "crates.io:cw3-fixed-multisig";
const CONTRACT_VERSION: &str = "1.0.0";
const MAX_PROPOSAL_SIZE: u64 = 30;
const MAX_DESC_LENGTH: u64 = 1024;

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    _info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    let total_weight = msg
        .signers
        .iter()
        .fold(Uint128::zero(), |acc, v| acc + v.weight);

    let cfg = Config {
        threshold: msg.threshold,
        max_voting_period: msg.max_voting_period,
        total_weight,
    };

    CONFIG.save(deps.storage, &cfg)?;

    for voter in msg.signers.iter() {
        let voter_addr = deps.api.addr_validate(&voter.addr)?;
        VOTERS.save(
            deps.storage,
            &voter_addr,
            &Voter {
                addr: voter.addr.clone(),
                weight: voter.weight,
            },
        )?;
    }

    PROPOSAL_COUNT.save(deps.storage, &0u64)?;

    Ok(Response::default()
        .add_attribute("method", "instantiate")
        .add_attribute("total_weight", total_weight)
        .add_attribute("signers_count", msg.signers.len().to_string()))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> StdResult<Response> {
    match msg {
        ExecuteMsg::Propose {
            title,
            description,
            msgs,
            earliest,
        } => execute_propose(deps, env, info, title, description, msgs, earliest),
        ExecuteMsg::Vote { proposal_id, vote } => {
            execute_vote(deps, env, info, proposal_id, vote)
        }
        ExecuteMsg::Execute { proposal_id } => {
            execute_execute(deps, env, info, proposal_id)
        }
        ExecuteMsg::Close { proposal_id } => {
            execute_close(deps, env, info, proposal_id)
        }
    }
}

pub fn execute_propose(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    title: String,
    description: String,
    msgs: Vec<CosmosMsg<Empty>>,
    earliest: Option<Expiration>,
) -> StdResult<Response> {
    let voter = VOTERS.may_load(deps.storage, &info.sender)?;
    if voter.is_none() {
        return Err(StdError::generic_err("Unauthorized: not a voter"));
    }

    if msgs.is_empty() {
        return Err(StdError::generic_err("Proposal must have at least one message"));
    }
    if msgs.len() > MAX_PROPOSAL_SIZE as usize {
        return Err(StdError::generic_err(
            format!("Proposal cannot have more than {} messages", MAX_PROPOSAL_SIZE),
        ));
    }
    if description.len() > MAX_DESC_LENGTH as usize {
        return Err(StdError::generic_err(
            format!("Description too long, max {} chars", MAX_DESC_LENGTH),
        ));
    }

    let cfg = CONFIG.load(deps.storage)?;
    let mut prop_count = PROPOSAL_COUNT.load(deps.storage)?;

    let expires = match &cfg.max_voting_period {
        Duration::Time(duration) => Expiration::AtTime(env.block.time.plus_seconds(*duration)),
        Duration::Height(duration) => Expiration::AtHeight(env.block.height + *duration),
    };

    prop_count += 1;
    PROPOSAL_COUNT.save(deps.storage, &prop_count)?;

    let voter = voter.unwrap();
    if voter.weight.is_zero() {
        return Err(StdError::generic_err("Voter has no weight"));
    }

    let proposal = Proposal {
        id: prop_count,
        title,
        description,
        msgs,
        status: ProposalStatus::Open,
        expires,
        threshold: cfg.threshold.clone(),
        total_weight: cfg.total_weight,
        yes_weight: voter.weight,
        no_weight: Uint128::zero(),
        veto_weight: Uint128::zero(),
        abstain_weight: Uint128::zero(),
        proposer: info.sender.clone(),
        created: env.block.time,
        executed_at: None,
    };

    PROPOSALS.save(deps.storage, prop_count, &proposal)?;

    let vote = Vote {
        proposal_id: prop_count,
        voter: info.sender.clone(),
        vote: VoteType::Yes,
        weight: voter.weight,
        voted_at: env.block.time,
    };
    VOTES.save(deps.storage, (prop_count, &info.sender), &vote)?;

    Ok(Response::new()
        .add_attribute("method", "propose")
        .add_attribute("proposal_id", prop_count.to_string())
        .add_attribute("proposer", info.sender.to_string())
        .add_attribute("yes_weight", proposal.yes_weight))
}

pub fn execute_vote(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    proposal_id: u64,
    vote: VoteType,
) -> StdResult<Response> {
    let voter = VOTERS.may_load(deps.storage, &info.sender)?;
    if voter.is_none() {
        return Err(StdError::generic_err("Unauthorized: not a voter"));
    }
    let voter = voter.unwrap();

    let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?;

    if proposal.status != ProposalStatus::Open {
        return Err(StdError::generic_err("Proposal is not open for voting"));
    }

    if proposal.expires.is_expired(&env.block) {
        proposal.status = ProposalStatus::Expired;
        PROPOSALS.save(deps.storage, proposal_id, &proposal)?;
        return Err(StdError::generic_err("Proposal has expired"));
    }

    if VOTES.has(deps.storage, (proposal_id, &info.sender)) {
        return Err(StdError::generic_err("Already voted on this proposal"));
    }

    let voter_weight = voter.weight;

    let vote_record = Vote {
        proposal_id,
        voter: info.sender.clone(),
        vote: vote.clone(),
        weight: voter_weight,
        voted_at: env.block.time,
    };
    VOTES.save(deps.storage, (proposal_id, &info.sender), &vote_record)?;

    match vote {
        VoteType::Yes => proposal.yes_weight += voter_weight,
        VoteType::No => proposal.no_weight += voter_weight,
        VoteType::Veto => proposal.veto_weight += voter_weight,
        VoteType::Abstain => proposal.abstain_weight += voter_weight,
    }

    let threshold_response = proposal.threshold.is_quorum(
        proposal.total_weight,
        proposal.yes_weight,
        proposal.no_weight,
        proposal.veto_weight,
        proposal.abstain_weight,
    );

    match threshold_response {
        ThresholdResponse::Passed => {
            proposal.status = ProposalStatus::Passed;
        }
        ThresholdResponse::Rejected { .. }
        | ThresholdResponse::Vetoed { .. } => {
            proposal.status = ProposalStatus::Rejected;
        }
        _ => {}
    }

    PROPOSALS.save(deps.storage, proposal_id, &proposal)?;

    Ok(Response::new()
        .add_attribute("method", "vote")
        .add_attribute("proposal_id", proposal_id.to_string())
        .add_attribute("voter", info.sender.to_string())
        .add_attribute("vote", format!("{:?}", vote))
        .add_attribute("status", format!("{:?}", proposal.status)))
}

pub fn execute_execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    proposal_id: u64,
) -> StdResult<Response> {
    let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?;

    if proposal.status != ProposalStatus::Passed {
        return Err(StdError::generic_err("Proposal is not in passed state"));
    }

    if proposal.expires.is_expired(&env.block) {
        proposal.status = ProposalStatus::Expired;
        PROPOSALS.save(deps.storage, proposal_id, &proposal)?;
        return Err(StdError::generic_err("Proposal has expired"));
    }

    proposal.status = ProposalStatus::Executed;
    proposal.executed_at = Some(env.block.time);
    PROPOSALS.save(deps.storage, proposal_id, &proposal)?;

    Ok(Response::new()
        .add_messages(proposal.msgs)
        .add_attribute("method", "execute")
        .add_attribute("proposal_id", proposal_id.to_string())
        .add_attribute("executor", info.sender.to_string()))
}

pub fn execute_close(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    proposal_id: u64,
) -> StdResult<Response> {
    let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?;

    if proposal.status == ProposalStatus::Executed {
        return Err(StdError::generic_err("Proposal already executed"));
    }
    if proposal.status == ProposalStatus::Closed {
        return Err(StdError::generic_err("Proposal already closed"));
    }

    if !proposal.expires.is_expired(&env.block) {
        return Err(StdError::generic_err("Proposal is not yet expired"));
    }

    proposal.status = ProposalStatus::Closed;
    PROPOSALS.save(deps.storage, proposal_id, &proposal)?;

    Ok(Response::new()
        .add_attribute("method", "close")
        .add_attribute("proposal_id", proposal_id.to_string()))
}

// ====== 查询接口 ======

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::Config {} => to_binary(&query_config(deps)?),
        QueryMsg::Voter { address } => to_binary(&query_voter(deps, address)?),
        QueryMsg::ListVoters { start_after, limit } => {
            to_binary(&query_list_voters(deps, start_after, limit)?)
        }
        QueryMsg::Proposal { proposal_id } => {
            to_binary(&query_proposal(deps, proposal_id)?)
        }
        QueryMsg::ListProposals { start_after, limit } => {
            to_binary(&query_list_proposals(deps, start_after, limit)?)
        }
        QueryMsg::ReverseProposals { start_before, limit } => {
            to_binary(&query_reverse_proposals(deps, start_before, limit)?)
        }
        QueryMsg::Vote { proposal_id, voter } => {
            to_binary(&query_vote(deps, proposal_id, voter)?)
        }
        QueryMsg::ListVotes { proposal_id, start_after, limit } => {
            to_binary(&query_list_votes(deps, proposal_id, start_after, limit)?)
        }
        QueryMsg::Threshold {} => to_binary(&query_threshold(deps)?),
        QueryMsg::ProposalCount {} => to_binary(&query_proposal_count(deps)?),
    }
}

pub fn query_config(deps: Deps) -> StdResult<ConfigResponse> {
    let cfg = CONFIG.load(deps.storage)?;
    Ok(ConfigResponse {
        threshold: cfg.threshold,
        max_voting_period: cfg.max_voting_period,
        total_weight: cfg.total_weight,
    })
}

pub fn query_voter(deps: Deps, address: String) -> StdResult<VoterResponse> {
    let addr = deps.api.addr_validate(&address)?;
    let voter = VOTERS.may_load(deps.storage, &addr)?;
    Ok(VoterResponse {
        weight: voter.as_ref().map(|v| v.weight).unwrap_or_default(),
    })
}

pub fn query_list_voters(
    deps: Deps,
    start_after: Option<String>,
    limit: Option<u32>,
) -> StdResult<VotersResponse> {
    let limit = limit.unwrap_or(30).min(100) as usize;
    let start = start_after.as_ref().map(|s| deps.api.addr_validate(s).unwrap());

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

    Ok(VotersResponse { voters })
}

pub fn query_proposal(deps: Deps, proposal_id: u64) -> StdResult<ProposalResponse> {
    let proposal = PROPOSALS.load(deps.storage, proposal_id)?;
    Ok(ProposalResponse {
        id: proposal.id,
        title: proposal.title,
        description: proposal.description,
        msgs: proposal.msgs,
        status: proposal.status,
        expires: proposal.expires,
        yes_weight: proposal.yes_weight,
        no_weight: proposal.no_weight,
        veto_weight: proposal.veto_weight,
        abstain_weight: proposal.abstain_weight,
        total_weight: proposal.total_weight,
        proposer: proposal.proposer.to_string(),
        created: proposal.created,
    })
}

pub fn query_list_proposals(
    deps: Deps,
    start_after: Option<u64>,
    limit: Option<u32>,
) -> StdResult<ProposalsResponse> {
    let limit = limit.unwrap_or(30).min(100) as usize;
    let start = start_after.unwrap_or(0);

    let proposals: Vec<ProposalResponse> = PROPOSALS
        .range(deps.storage, Some(start + 1), None, Order::Ascending)
        .take(limit)
        .map(|item| {
            let (id, proposal) = item?;
            Ok(ProposalResponse {
                id,
                title: proposal.title,
                description: proposal.description,
                msgs: proposal.msgs,
                status: proposal.status,
                expires: proposal.expires,
                yes_weight: proposal.yes_weight,
                no_weight: proposal.no_weight,
                veto_weight: proposal.veto_weight,
                abstain_weight: proposal.abstain_weight,
                total_weight: proposal.total_weight,
                proposer: proposal.proposer.to_string(),
                created: proposal.created,
            })
        })
        .collect::<StdResult<Vec<_>>>()?;

    Ok(ProposalsResponse { proposals })
}

pub fn query_reverse_proposals(
    deps: Deps,
    start_before: Option<u64>,
    limit: Option<u32>,
) -> StdResult<ProposalsResponse> {
    let limit = limit.unwrap_or(30).min(100) as usize;
    let end = start_before.unwrap_or(u64::MAX);

    let proposals: Vec<ProposalResponse> = PROPOSALS
        .range(deps.storage, None, Some(end), Order::Descending)
        .take(limit)
        .map(|item| {
            let (id, proposal) = item?;
            Ok(ProposalResponse {
                id,
                title: proposal.title,
                description: proposal.description,
                msgs: proposal.msgs,
                status: proposal.status,
                expires: proposal.expires,
                yes_weight: proposal.yes_weight,
                no_weight: proposal.no_weight,
                veto_weight: proposal.veto_weight,
                abstain_weight: proposal.abstain_weight,
                total_weight: proposal.total_weight,
                proposer: proposal.proposer.to_string(),
                created: proposal.created,
            })
        })
        .collect::<StdResult<Vec<_>>>()?;

    Ok(ProposalsResponse { proposals })
}

pub fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult<VoteResponse> {
    let addr = deps.api.addr_validate(&voter)?;
    let vote = VOTES.may_load(deps.storage, (proposal_id, &addr))?;
    match vote {
        Some(v) => Ok(VoteResponse {
            vote: Some(v.vote),
            weight: v.weight,
        }),
        None => Ok(VoteResponse {
            vote: None,
            weight: Uint128::zero(),
        }),
    }
}

pub fn query_list_votes(
    deps: Deps,
    proposal_id: u64,
    start_after: Option<String>,
    limit: Option<u32>,
) -> StdResult<VotesResponse> {
    let limit = limit.unwrap_or(30).min(100) as usize;
    let start = start_after.as_ref().map(|s| deps.api.addr_validate(s).unwrap());

    let votes: Vec<VoteInfo> = VOTES
        .range(deps.storage, None, None, Order::Ascending)
        .filter(|item| {
            if let Ok(((pid, _), _)) = item {
                *pid == proposal_id
            } else {
                false
            }
        })
        .take(limit)
        .map(|item| {
            let ((_, voter_addr), vote) = item?;
            Ok(VoteInfo {
                voter: voter_addr.to_string(),
                vote: vote.vote,
                weight: vote.weight,
            })
        })
        .collect::<StdResult<Vec<_>>>()?;

    Ok(VotesResponse { votes })
}

pub fn query_threshold(deps: Deps) -> StdResult<ThresholdResponse> {
    let cfg = CONFIG.load(deps.storage)?;
    Ok(ThresholdResponse {
        threshold: cfg.threshold,
        total_weight: cfg.total_weight,
    })
}

pub fn query_proposal_count(deps: Deps) -> StdResult<ProposalCountResponse> {
    let count = PROPOSALS
        .range(deps.storage, None, None, Order::Ascending)
        .count() as u64;
    Ok(ProposalCountResponse { count })
}

2.3 单元测试

#[cfg(test)]
mod tests {
    use super::*;
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::{coins, from_binary, BankMsg, CosmosMsg, Uint128};
    use cw_utils::{Duration, Threshold};

    fn create_test_voters() -> Vec<Voter> {
        vec![
            Voter {
                addr: "msg1creator".to_string(),
                weight: Uint128::new(1),
            },
            Voter {
                addr: "msg1voter1".to_string(),
                weight: Uint128::new(1),
            },
            Voter {
                addr: "msg1voter2".to_string(),
                weight: Uint128::new(1),
            },
            Voter {
                addr: "msg1voter3".to_string(),
                weight: Uint128::new(1),
            },
            Voter {
                addr: "msg1voter4".to_string(),
                weight: Uint128::new(1),
            },
        ]
    }

    fn setup_contract(deps: DepsMut) {
        let instantiate_msg = InstantiateMsg {
            signers: create_test_voters(),
            threshold: Threshold::AbsoluteCount {
                weight: Uint128::new(3),
            },
            max_voting_period: Duration::Time(604800),
        };

        let info = mock_info("msg1creator", &[]);
        let env = mock_env();
        instantiate(deps, env, info, instantiate_msg).unwrap();
    }

    #[test]
    fn test_instantiate() {
        let mut deps = mock_dependencies();
        setup_contract(deps.as_mut());

        let config = CONFIG.load(&deps.storage).unwrap();
        assert_eq!(config.total_weight, Uint128::new(5));
    }

    #[test]
    fn test_propose() {
        let mut deps = mock_dependencies();
        setup_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1creator", &[]);

        let propose_msg = ExecuteMsg::Propose {
            title: "Transfer 100 MSG".to_string(),
            description: "Payment for Q1 development".to_string(),
            msgs: vec![CosmosMsg::Bank(BankMsg::Send {
                to_address: "msg1recipient".to_string(),
                amount: coins(100000000, "umsg"),
            })],
            earliest: None,
        };

        let res = execute(deps.as_mut(), env, info, propose_msg).unwrap();
        assert_eq!(res.attributes[1].value, "1");

        let proposal = PROPOSALS.load(&deps.storage, 1).unwrap();
        assert_eq!(proposal.status, ProposalStatus::Open);
        assert_eq!(proposal.yes_weight, Uint128::new(1));
        assert_eq!(proposal.title, "Transfer 100 MSG");
    }

    #[test]
    fn test_vote_and_execute() {
        let mut deps = mock_dependencies();
        setup_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1creator", &[]);
        let propose_msg = ExecuteMsg::Propose {
            title: "Test Transfer".to_string(),
            description: "Test".to_string(),
            msgs: vec![CosmosMsg::Bank(BankMsg::Send {
                to_address: "msg1recipient".to_string(),
                amount: coins(1000, "umsg"),
            })],
            earliest: None,
        };
        execute(deps.as_mut(), env, info, propose_msg).unwrap();

        assert_eq!(PROPOSALS.load(&deps.storage, 1).unwrap().yes_weight, Uint128::new(1));

        let env = mock_env();
        let info = mock_info("msg1voter1", &[]);
        let vote_msg = ExecuteMsg::Vote {
            proposal_id: 1,
            vote: VoteType::Yes,
        };
        execute(deps.as_mut(), env, info, vote_msg).unwrap();

        let env = mock_env();
        let info = mock_info("msg1voter2", &[]);
        let vote_msg = ExecuteMsg::Vote {
            proposal_id: 1,
            vote: VoteType::Yes,
        };
        execute(deps.as_mut(), env, info, vote_msg).unwrap();

        let proposal = PROPOSALS.load(&deps.storage, 1).unwrap();
        assert_eq!(proposal.status, ProposalStatus::Passed);
        assert_eq!(proposal.yes_weight, Uint128::new(3));

        let env = mock_env();
        let info = mock_info("msg1executor", &[]);
        let exec_msg = ExecuteMsg::Execute { proposal_id: 1 };
        let res = execute(deps.as_mut(), env, info, exec_msg).unwrap();
        assert_eq!(res.messages.len(), 1);

        let proposal = PROPOSALS.load(&deps.storage, 1).unwrap();
        assert_eq!(proposal.status, ProposalStatus::Executed);
    }

    #[test]
    fn test_veto_rejection() {
        let mut deps = mock_dependencies();
        setup_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1creator", &[]);
        let propose_msg = ExecuteMsg::Propose {
            title: "Bad Proposal".to_string(),
            description: "Should be vetoed".to_string(),
            msgs: vec![CosmosMsg::Bank(BankMsg::Send {
                to_address: "msg1bad".to_string(),
                amount: coins(999999, "umsg"),
            })],
            earliest: None,
        };
        execute(deps.as_mut(), env, info, propose_msg).unwrap();

        let voters_nay = vec!["msg1voter1", "msg1voter2", "msg1voter3"];
        for v in voters_nay {
            let env = mock_env();
            let info = mock_info(v, &[]);
            let vote_msg = ExecuteMsg::Vote {
                proposal_id: 1,
                vote: VoteType::No,
            };
            execute(deps.as_mut(), env, info, vote_msg).unwrap();
        }

        let proposal = PROPOSALS.load(&deps.storage, 1).unwrap();
        assert_eq!(proposal.status, ProposalStatus::Rejected);
    }

    #[test]
    fn test_close_expired_proposal() {
        let mut deps = mock_dependencies();
        setup_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1creator", &[]);
        let propose_msg = ExecuteMsg::Propose {
            title: "Old Proposal".to_string(),
            description: "Should expire".to_string(),
            msgs: vec![CosmosMsg::Bank(BankMsg::Send {
                to_address: "msg1recipient".to_string(),
                amount: coins(100, "umsg"),
            })],
            earliest: None,
        };
        execute(deps.as_mut(), env, info, propose_msg).unwrap();

        let mut env = mock_env();
        env.block.time = env.block.time.plus_seconds(700000);

        let info = mock_info("msg1voter1", &[]);
        let vote_msg = ExecuteMsg::Vote {
            proposal_id: 1,
            vote: VoteType::Yes,
        };
        let err = execute(deps.as_mut(), env.clone(), info, vote_msg).unwrap_err();
        assert_eq!(err, StdError::generic_err("Proposal has expired"));

        let info = mock_info("msg1closer", &[]);
        let close_msg = ExecuteMsg::Close { proposal_id: 1 };
        execute(deps.as_mut(), env, info, close_msg).unwrap();

        let proposal = PROPOSALS.load(&deps.storage, 1).unwrap();
        assert_eq!(proposal.status, ProposalStatus::Closed);
    }

    #[test]
    fn test_unauthorized_voter() {
        let mut deps = mock_dependencies();
        setup_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1stranger", &[]);
        let propose_msg = ExecuteMsg::Propose {
            title: "Hack".to_string(),
            description: "Steal funds".to_string(),
            msgs: vec![],
            earliest: None,
        };
        let err = execute(deps.as_mut(), env, info, propose_msg).unwrap_err();
        assert_eq!(err, StdError::generic_err("Unauthorized: not a voter"));
    }

    #[test]
    fn test_double_vote_rejected() {
        let mut deps = mock_dependencies();
        setup_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1creator", &[]);
        let propose_msg = ExecuteMsg::Propose {
            title: "Test".to_string(),
            description: "Test double vote".to_string(),
            msgs: vec![CosmosMsg::Bank(BankMsg::Send {
                to_address: "msg1recipient".to_string(),
                amount: coins(100, "umsg"),
            })],
            earliest: None,
        };
        execute(deps.as_mut(), env, info, propose_msg).unwrap();

        let env = mock_env();
        let info = mock_info("msg1voter1", &[]);
        let vote_msg = ExecuteMsg::Vote {
            proposal_id: 1,
            vote: VoteType::Yes,
        };
        execute(deps.as_mut(), env.clone(), info.clone(), vote_msg).unwrap();

        let vote_msg2 = ExecuteMsg::Vote {
            proposal_id: 1,
            vote: VoteType::No,
        };
        let err = execute(deps.as_mut(), env, info, vote_msg2).unwrap_err();
        assert_eq!(err, StdError::generic_err("Already voted on this proposal"));
    }

    #[test]
    fn test_weighted_voting() {
        let mut deps = mock_dependencies();

        let voters = vec![
            Voter {
                addr: "msg1large".to_string(),
                weight: Uint128::new(5),
            },
            Voter {
                addr: "msg1small1".to_string(),
                weight: Uint128::new(1),
            },
            Voter {
                addr: "msg1small2".to_string(),
                weight: Uint128::new(1),
            },
        ];

        let instantiate_msg = InstantiateMsg {
            signers: voters,
            threshold: Threshold::AbsoluteCount {
                weight: Uint128::new(5),
            },
            max_voting_period: Duration::Time(86400),
        };

        let info = mock_info("msg1large", &[]);
        let env = mock_env();
        let deps_mut = deps.as_mut();
        instantiate(deps_mut, env, info, instantiate_msg).unwrap();

        let env = mock_env();
        let info = mock_info("msg1large", &[]);
        let propose_msg = ExecuteMsg::Propose {
            title: "Large voter passes".to_string(),
            description: "Weight 5 passes threshold 5".to_string(),
            msgs: vec![CosmosMsg::Bank(BankMsg::Send {
                to_address: "msg1dest".to_string(),
                amount: coins(500, "umsg"),
            })],
            earliest: None,
        };
        let res = execute(deps.as_mut(), env, info, propose_msg).unwrap();

        let proposal = PROPOSALS.load(&deps.storage, 1).unwrap();
        assert_eq!(proposal.status, ProposalStatus::Passed);
    }
}

2.4 消息与查询定义

use cosmwasm_std::{CosmosMsg, Empty};
use cw_utils::{Duration, Expiration, Threshold};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
    pub signers: Vec<Voter>,
    pub threshold: Threshold,
    pub max_voting_period: Duration,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
    Propose {
        title: String,
        description: String,
        msgs: Vec<CosmosMsg<Empty>>,
        earliest: Option<Expiration>,
    },
    Vote {
        proposal_id: u64,
        vote: VoteType,
    },
    Execute {
        proposal_id: u64,
    },
    Close {
        proposal_id: u64,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
    Config {},
    Voter { address: String },
    ListVoters {
        start_after: Option<String>,
        limit: Option<u32>,
    },
    Proposal {
        proposal_id: u64,
    },
    ListProposals {
        start_after: Option<u64>,
        limit: Option<u32>,
    },
    ReverseProposals {
        start_before: Option<u64>,
        limit: Option<u32>,
    },
    Vote {
        proposal_id: u64,
        voter: String,
    },
    ListVotes {
        proposal_id: u64,
        start_after: Option<String>,
        limit: Option<u32>,
    },
    Threshold {},
    ProposalCount {},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ConfigResponse {
    pub threshold: Threshold,
    pub max_voting_period: Duration,
    pub total_weight: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VoterResponse {
    pub weight: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VoterDetail {
    pub addr: String,
    pub weight: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VotersResponse {
    pub voters: Vec<VoterDetail>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ProposalResponse {
    pub id: u64,
    pub title: String,
    pub description: String,
    pub msgs: Vec<CosmosMsg<Empty>>,
    pub status: ProposalStatus,
    pub expires: Expiration,
    pub yes_weight: Uint128,
    pub no_weight: Uint128,
    pub veto_weight: Uint128,
    pub abstain_weight: Uint128,
    pub total_weight: Uint128,
    pub proposer: String,
    pub created: Timestamp,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ProposalsResponse {
    pub proposals: Vec<ProposalResponse>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VoteResponse {
    pub vote: Option<VoteType>,
    pub weight: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VoteInfo {
    pub voter: String,
    pub vote: VoteType,
    pub weight: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct VotesResponse {
    pub votes: Vec<VoteInfo>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ThresholdResponse {
    pub threshold: Threshold,
    pub total_weight: Uint128,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct ProposalCountResponse {
    pub count: u64,
}

3. 合约完整实现 — cw3_flex_multisig

3.1 Flex Multisig 设计

cw3_flex_multisig 在 cw3_fixed_multisig 的基础上增加了以下能力:

  1. 动态成员管理:通过提案添加或移除签名者
  2. 阈值调整:通过提案修改阈值
  3. 权重调整:通过提案调整成员的投票权重

3.2 额外存储结构

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FlexExecuteMsg {
    Propose {
        title: String,
        description: String,
        msgs: Vec<CosmosMsg<Empty>>,
        earliest: Option<Expiration>,
    },
    Vote {
        proposal_id: u64,
        vote: VoteType,
    },
    Execute {
        proposal_id: u64,
    },
    Close {
        proposal_id: u64,
    },
    UpdateConfig {
        threshold: Option<Threshold>,
        max_voting_period: Option<Duration>,
    },
    AddVoter {
        addr: String,
        weight: Option<Uint128>,
    },
    RemoveVoter {
        addr: String,
    },
    UpdateVoterWeight {
        addr: String,
        weight: Uint128,
    },
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MemberChangeProposal {
    pub additions: Vec<Voter>,
    pub removals: Vec<String>,
    pub weight_changes: Vec<(String, Uint128)>,
    pub threshold_change: Option<Threshold>,
}

pub const OWNER: Item<Addr> = Item::new("owner");
pub const MEMBER_CHANGE_PROPOSALS: Map<u64, MemberChangeProposal> =
    Map::new("member_changes");

3.3 Flex Multisig 完整实现

use cosmwasm_std::{
    entry_point, to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut,
    Empty, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128,
};
use cw2::set_contract_version;
use cw_utils::{Duration, Expiration, Threshold, ThresholdResponse};

const CONTRACT_NAME: &str = "crates.io:cw3-flex-multisig";
const CONTRACT_VERSION: &str = "1.0.0";

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msg: InstantiateMsg,
) -> StdResult<Response> {
    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;

    let total_weight = msg
        .signers
        .iter()
        .fold(Uint128::zero(), |acc, v| acc + v.weight);

    let cfg = Config {
        threshold: msg.threshold,
        max_voting_period: msg.max_voting_period,
        total_weight,
    };

    CONFIG.save(deps.storage, &cfg)?;

    for voter in msg.signers.iter() {
        let voter_addr = deps.api.addr_validate(&voter.addr)?;
        VOTERS.save(
            deps.storage,
            &voter_addr,
            &Voter {
                addr: voter.addr.clone(),
                weight: voter.weight,
            },
        )?;
    }

    PROPOSAL_COUNT.save(deps.storage, &0u64)?;
    OWNER.save(deps.storage, &info.sender)?;

    Ok(Response::default()
        .add_attribute("method", "instantiate")
        .add_attribute("owner", info.sender.to_string())
        .add_attribute("total_weight", total_weight)
        .add_attribute("voters", msg.signers.len().to_string()))
}

fn is_voter(deps: &DepsMut, addr: &Addr) -> bool {
    VOTERS.has(deps.storage, addr)
}

fn add_voter(deps: DepsMut, addr: &Addr, weight: Uint128) -> StdResult<()> {
    if weight.is_zero() {
        return Err(StdError::generic_err("Voter weight cannot be zero"));
    }
    if is_voter(&deps, addr) {
        return Err(StdError::generic_err("Voter already exists"));
    }

    VOTERS.save(
        deps.storage,
        addr,
        &Voter {
            addr: addr.to_string(),
            weight,
        },
    )?;

    let mut cfg = CONFIG.load(deps.storage)?;
    cfg.total_weight += weight;
    CONFIG.save(deps.storage, &cfg)?;

    Ok(())
}

fn remove_voter(deps: DepsMut, addr: &Addr) -> StdResult<()> {
    let voter = VOTERS.may_load(deps.storage, addr)?;
    match voter {
        Some(v) => {
            VOTERS.remove(deps.storage, addr);
            let mut cfg = CONFIG.load(deps.storage)?;
            cfg.total_weight -= v.weight;
            CONFIG.save(deps.storage, &cfg)?;
            Ok(())
        }
        None => Err(StdError::generic_err("Voter not found")),
    }
}

fn update_voter_weight(deps: DepsMut, addr: &Addr, new_weight: Uint128) -> StdResult<()> {
    if new_weight.is_zero() {
        return Err(StdError::generic_err("Voter weight cannot be zero"));
    }

    let voter = VOTERS.may_load(deps.storage, addr)?;
    match voter {
        Some(mut v) => {
            let old_weight = v.weight;
            v.weight = new_weight;
            VOTERS.save(deps.storage, addr, &v)?;

            let mut cfg = CONFIG.load(deps.storage)?;
            cfg.total_weight = cfg.total_weight - old_weight + new_weight;
            CONFIG.save(deps.storage, &cfg)?;
            Ok(())
        }
        None => Err(StdError::generic_err("Voter not found")),
    }
}

fn update_threshold(deps: DepsMut, threshold: Threshold) -> StdResult<()> {
    let mut cfg = CONFIG.load(deps.storage)?;
    cfg.threshold = threshold;
    CONFIG.save(deps.storage, &cfg)?;
    Ok(())
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    msg: ExecuteMsg,
) -> StdResult<Response> {
    match msg {
        ExecuteMsg::Propose { title, description, msgs, earliest } => {
            execute_propose(deps, env, info, title, description, msgs, earliest)
        }
        ExecuteMsg::Vote { proposal_id, vote } => {
            execute_vote(deps, env, info, proposal_id, vote)
        }
        ExecuteMsg::Execute { proposal_id } => {
            execute_execute(deps, env, info, proposal_id)
        }
        ExecuteMsg::Close { proposal_id } => {
            execute_close(deps, env, info, proposal_id)
        }
        ExecuteMsg::UpdateConfig {
            threshold,
            max_voting_period,
        } => execute_update_config(deps, env, info, threshold, max_voting_period),
        ExecuteMsg::AddVoter { addr, weight } => {
            execute_add_voter(deps, env, info, addr, weight)
        }
        ExecuteMsg::RemoveVoter { addr } => {
            execute_remove_voter(deps, env, info, addr)
        }
        ExecuteMsg::UpdateVoterWeight { addr, weight } => {
            execute_update_voter_weight(deps, env, info, addr, weight)
        }
    }
}

pub fn execute_update_config(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    threshold: Option<Threshold>,
    max_voting_period: Option<Duration>,
) -> StdResult<Response> {
    let owner = OWNER.load(deps.storage)?;
    if info.sender != owner {
        return Err(StdError::generic_err("Unauthorized: only owner can update config"));
    }

    let mut cfg = CONFIG.load(deps.storage)?;
    if let Some(t) = threshold {
        cfg.threshold = t;
    }
    if let Some(d) = max_voting_period {
        cfg.max_voting_period = d;
    }
    CONFIG.save(deps.storage, &cfg)?;

    Ok(Response::new()
        .add_attribute("method", "update_config")
        .add_attribute("sender", info.sender.to_string()))
}

pub fn execute_add_voter(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    addr: String,
    weight: Option<Uint128>,
) -> StdResult<Response> {
    let owner = OWNER.load(deps.storage)?;
    if info.sender != owner {
        return Err(StdError::generic_err("Unauthorized: only owner can add voters"));
    }

    let voter_addr = deps.api.addr_validate(&addr)?;
    let voter_weight = weight.unwrap_or(Uint128::new(1));
    add_voter(deps, &voter_addr, voter_weight)?;

    Ok(Response::new()
        .add_attribute("method", "add_voter")
        .add_attribute("addr", addr)
        .add_attribute("weight", voter_weight.to_string()))
}

pub fn execute_remove_voter(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    addr: String,
) -> StdResult<Response> {
    let owner = OWNER.load(deps.storage)?;
    if info.sender != owner {
        return Err(StdError::generic_err("Unauthorized: only owner can remove voters"));
    }

    let voter_addr = deps.api.addr_validate(&addr)?;
    remove_voter(deps, &voter_addr)?;

    Ok(Response::new()
        .add_attribute("method", "remove_voter")
        .add_attribute("addr", addr))
}

pub fn execute_update_voter_weight(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    addr: String,
    weight: Uint128,
) -> StdResult<Response> {
    let owner = OWNER.load(deps.storage)?;
    if info.sender != owner {
        return Err(StdError::generic_err("Unauthorized: only owner can change voter weights"));
    }

    let voter_addr = deps.api.addr_validate(&addr)?;
    update_voter_weight(deps, &voter_addr, weight)?;

    Ok(Response::new()
        .add_attribute("method", "update_voter_weight")
        .add_attribute("addr", addr)
        .add_attribute("new_weight", weight.to_string()))
}

pub fn execute_propose(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    title: String,
    description: String,
    msgs: Vec<CosmosMsg<Empty>>,
    earliest: Option<Expiration>,
) -> StdResult<Response> {
    let voter = VOTERS.may_load(deps.storage, &info.sender)?;
    if voter.is_none() {
        return Err(StdError::generic_err("Unauthorized: not a voter"));
    }
    let voter = voter.unwrap();

    if msgs.is_empty() {
        return Err(StdError::generic_err("Proposal must have at least one message"));
    }

    let cfg = CONFIG.load(deps.storage)?;
    let mut prop_count = PROPOSAL_COUNT.load(deps.storage)?;

    let expires = match &cfg.max_voting_period {
        Duration::Time(duration) => Expiration::AtTime(env.block.time.plus_seconds(*duration)),
        Duration::Height(duration) => Expiration::AtHeight(env.block.height + *duration),
    };

    prop_count += 1;
    PROPOSAL_COUNT.save(deps.storage, &prop_count)?;

    if voter.weight.is_zero() {
        return Err(StdError::generic_err("Voter has no weight"));
    }

    let proposal = Proposal {
        id: prop_count,
        title,
        description,
        msgs,
        status: ProposalStatus::Open,
        expires,
        threshold: cfg.threshold.clone(),
        total_weight: cfg.total_weight,
        yes_weight: voter.weight,
        no_weight: Uint128::zero(),
        veto_weight: Uint128::zero(),
        abstain_weight: Uint128::zero(),
        proposer: info.sender.clone(),
        created: env.block.time,
    };

    PROPOSALS.save(deps.storage, prop_count, &proposal)?;

    let vote = Vote {
        proposal_id: prop_count,
        voter: info.sender.clone(),
        vote: VoteType::Yes,
        weight: voter.weight,
        voted_at: env.block.time,
    };
    VOTES.save(deps.storage, (prop_count, &info.sender), &vote)?;

    Ok(Response::new()
        .add_attribute("method", "propose")
        .add_attribute("proposal_id", prop_count.to_string())
        .add_attribute("proposer", info.sender.to_string()))
}

pub fn execute_vote(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    proposal_id: u64,
    vote: VoteType,
) -> StdResult<Response> {
    let voter = VOTERS.may_load(deps.storage, &info.sender)?;
    if voter.is_none() {
        return Err(StdError::generic_err("Unauthorized: not a voter"));
    }
    let voter = voter.unwrap();

    let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?;

    if proposal.status != ProposalStatus::Open {
        return Err(StdError::generic_err("Proposal is not open for voting"));
    }

    if proposal.expires.is_expired(&env.block) {
        proposal.status = ProposalStatus::Expired;
        PROPOSALS.save(deps.storage, proposal_id, &proposal)?;
        return Err(StdError::generic_err("Proposal has expired"));
    }

    if VOTES.has(deps.storage, (proposal_id, &info.sender)) {
        return Err(StdError::generic_err("Already voted on this proposal"));
    }

    let voter_weight = voter.weight;

    let vote_record = Vote {
        proposal_id,
        voter: info.sender.clone(),
        vote: vote.clone(),
        weight: voter_weight,
        voted_at: env.block.time,
    };
    VOTES.save(deps.storage, (proposal_id, &info.sender), &vote_record)?;

    match vote {
        VoteType::Yes => proposal.yes_weight += voter_weight,
        VoteType::No => proposal.no_weight += voter_weight,
        VoteType::Veto => proposal.veto_weight += voter_weight,
        VoteType::Abstain => proposal.abstain_weight += voter_weight,
    }

    let current_cfg = CONFIG.load(deps.storage)?;
    let threshold_response = current_cfg.threshold.is_quorum(
        proposal.total_weight,
        proposal.yes_weight,
        proposal.no_weight,
        proposal.veto_weight,
        proposal.abstain_weight,
    );

    match threshold_response {
        ThresholdResponse::Passed => {
            proposal.status = ProposalStatus::Passed;
        }
        ThresholdResponse::Rejected { .. } | ThresholdResponse::Vetoed { .. } => {
            proposal.status = ProposalStatus::Rejected;
        }
        _ => {}
    }

    PROPOSALS.save(deps.storage, proposal_id, &proposal)?;

    Ok(Response::new()
        .add_attribute("method", "vote")
        .add_attribute("proposal_id", proposal_id.to_string())
        .add_attribute("voter", info.sender.to_string())
        .add_attribute("vote", format!("{:?}", vote)))
}

pub fn execute_execute(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    proposal_id: u64,
) -> StdResult<Response> {
    let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?;

    if proposal.status != ProposalStatus::Passed {
        return Err(StdError::generic_err("Proposal is not in passed state"));
    }

    if proposal.expires.is_expired(&env.block) {
        proposal.status = ProposalStatus::Expired;
        PROPOSALS.save(deps.storage, proposal_id, &proposal)?;
        return Err(StdError::generic_err("Proposal has expired"));
    }

    proposal.status = ProposalStatus::Executed;
    PROPOSALS.save(deps.storage, proposal_id, &proposal)?;

    Ok(Response::new()
        .add_messages(proposal.msgs)
        .add_attribute("method", "execute")
        .add_attribute("proposal_id", proposal_id.to_string())
        .add_attribute("executor", info.sender.to_string()))
}

pub fn execute_close(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    proposal_id: u64,
) -> StdResult<Response> {
    let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?;

    if proposal.status == ProposalStatus::Executed {
        return Err(StdError::generic_err("Proposal already executed"));
    }
    if proposal.status == ProposalStatus::Closed {
        return Err(StdError::generic_err("Proposal already closed"));
    }
    if !proposal.expires.is_expired(&env.block) {
        return Err(StdError::generic_err("Proposal is not yet expired"));
    }

    proposal.status = ProposalStatus::Closed;
    PROPOSALS.save(deps.storage, proposal_id, &proposal)?;

    Ok(Response::new()
        .add_attribute("method", "close")
        .add_attribute("proposal_id", proposal_id.to_string()))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
    match msg {
        QueryMsg::Config {} => to_binary(&query_config(deps)?),
        QueryMsg::Voter { address } => to_binary(&query_voter(deps, address)?),
        QueryMsg::ListVoters { start_after, limit } => {
            to_binary(&query_list_voters(deps, start_after, limit)?)
        }
        QueryMsg::Proposal { proposal_id } => {
            to_binary(&query_proposal(deps, proposal_id)?)
        }
        QueryMsg::ListProposals { start_after, limit } => {
            to_binary(&query_list_proposals(deps, start_after, limit)?)
        }
        QueryMsg::ReverseProposals { start_before, limit } => {
            to_binary(&query_reverse_proposals(deps, start_before, limit)?)
        }
        QueryMsg::Vote { proposal_id, voter } => {
            to_binary(&query_vote(deps, proposal_id, voter)?)
        }
        QueryMsg::ListVotes { proposal_id, start_after, limit } => {
            to_binary(&query_list_votes(deps, proposal_id, start_after, limit)?)
        }
        QueryMsg::Threshold {} => to_binary(&query_threshold(deps)?),
        QueryMsg::ProposalCount {} => to_binary(&query_proposal_count(deps)?),
        QueryMsg::Owner {} => to_binary(&query_owner(deps)?),
        QueryMsg::MemberHistory { start_after, limit } => {
            to_binary(&query_member_history(deps, start_after, limit)?)
        }
    }
}

fn query_owner(deps: Deps) -> StdResult<OwnerResponse> {
    let owner = OWNER.load(deps.storage)?;
    Ok(OwnerResponse {
        owner: owner.to_string(),
    })
}

fn query_member_history(
    deps: Deps,
    start_after: Option<u64>,
    limit: Option<u32>,
) -> StdResult<MemberHistoryResponse> {
    let limit = limit.unwrap_or(20).min(50) as usize;
    let start = start_after.unwrap_or(0);

    let changes: Vec<MemberChangeRecord> = MEMBER_CHANGE_PROPOSALS
        .range(deps.storage, Some(start + 1), None, Order::Ascending)
        .take(limit)
        .map(|item| {
            let (id, change) = item?;
            Ok(MemberChangeRecord {
                proposal_id: id,
                additions: change.additions,
                removals: change.removals,
                weight_changes: change.weight_changes,
                threshold_change: change.threshold_change,
            })
        })
        .collect::<StdResult<Vec<_>>>()?;

    Ok(MemberHistoryResponse { changes })
}

3.4 Flex 查询响应

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct OwnerResponse {
    pub owner: String,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MemberChangeRecord {
    pub proposal_id: u64,
    pub additions: Vec<Voter>,
    pub removals: Vec<String>,
    pub weight_changes: Vec<(String, Uint128)>,
    pub threshold_change: Option<Threshold>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MemberHistoryResponse {
    pub changes: Vec<MemberChangeRecord>,
}

3.5 Flex Multisig 测试

#[cfg(test)]
mod flex_tests {
    use super::*;
    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
    use cosmwasm_std::{coins, BankMsg, CosmosMsg};

    fn setup_flex_contract(deps: DepsMut) {
        let voters = vec![
            Voter { addr: "msg1owner".to_string(), weight: Uint128::new(2) },
            Voter { addr: "msg1alice".to_string(), weight: Uint128::new(1) },
            Voter { addr: "msg1bob".to_string(), weight: Uint128::new(1) },
        ];

        let msg = InstantiateMsg {
            signers: voters,
            threshold: Threshold::AbsoluteCount { weight: Uint128::new(2) },
            max_voting_period: Duration::Time(86400),
        };

        let info = mock_info("msg1owner", &[]);
        let env = mock_env();
        instantiate(deps, env, info, msg).unwrap();
    }

    #[test]
    fn test_add_voter() {
        let mut deps = mock_dependencies();
        setup_flex_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1owner", &[]);
        let msg = ExecuteMsg::AddVoter {
            addr: "msg1charlie".to_string(),
            weight: Some(Uint128::new(1)),
        };

        execute(deps.as_mut(), env, info, msg).unwrap();

        let voter = query_voter(deps.as_ref(), "msg1charlie".to_string()).unwrap();
        assert_eq!(voter.weight, Uint128::new(1));
    }

    #[test]
    fn test_remove_voter() {
        let mut deps = mock_dependencies();
        setup_flex_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1owner", &[]);
        let msg = ExecuteMsg::RemoveVoter {
            addr: "msg1alice".to_string(),
        };

        execute(deps.as_mut(), env, info, msg).unwrap();

        let voter = query_voter(deps.as_ref(), "msg1alice".to_string()).unwrap();
        assert_eq!(voter.weight, Uint128::zero());
    }

    #[test]
    fn test_unauthorized_add_voter() {
        let mut deps = mock_dependencies();
        setup_flex_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1alice", &[]);
        let msg = ExecuteMsg::AddVoter {
            addr: "msg1mallory".to_string(),
            weight: None,
        };

        let err = execute(deps.as_mut(), env, info, msg).unwrap_err();
        assert!(err.to_string().contains("Unauthorized"));
    }

    #[test]
    fn test_update_threshold() {
        let mut deps = mock_dependencies();
        setup_flex_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1owner", &[]);
        let msg = ExecuteMsg::UpdateConfig {
            threshold: Some(Threshold::AbsoluteCount { weight: Uint128::new(3) }),
            max_voting_period: None,
        };

        execute(deps.as_mut(), env, info, msg).unwrap();

        let cfg = CONFIG.load(&deps.storage).unwrap();
        match cfg.threshold {
            Threshold::AbsoluteCount { weight } => assert_eq!(weight, Uint128::new(3)),
            _ => panic!("wrong threshold type"),
        }
    }

    #[test]
    fn test_dynamic_voter_affects_voting() {
        let mut deps = mock_dependencies();
        setup_flex_contract(deps.as_mut());

        let env = mock_env();
        let info = mock_info("msg1owner", &[]);
        let msg = ExecuteMsg::AddVoter {
            addr: "msg1heavy".to_string(),
            weight: Some(Uint128::new(5)),
        };
        execute(deps.as_mut(), env, info, msg).unwrap();

        let env = mock_env();
        let info = mock_info("msg1heavy", &[]);
        let propose_msg = ExecuteMsg::Propose {
            title: "Heavy weight passes".to_string(),
            description: "Weight 5 >= threshold 2".to_string(),
            msgs: vec![CosmosMsg::Bank(BankMsg::Send {
                to_address: "msg1dest".to_string(),
                amount: coins(100, "umsg"),
            })],
            earliest: None,
        };
        execute(deps.as_mut(), env, info, propose_msg).unwrap();

        let proposal = PROPOSALS.load(&deps.storage, 1).unwrap();
        assert_eq!(proposal.status, ProposalStatus::Passed);
    }
}

4. 多签操作流程

4.1 MSG Chain 环境准备

安装 msg-chain-devkit

curl -sSL https://msgchain.org/install.sh | bash
msg-chain-devkit --version
msg-chain-devkit config set chain-id msg-chain-1
msg-chain-devkit config set node https://rpc.msgchain.org

创建多签账户

msg-chain-devkit keys add signer1
msg-chain-devkit keys add signer2
msg-chain-devkit keys add signer3
msg-chain-devkit keys add signer4
msg-chain-devkit keys add signer5
msg-chain-devkit keys list

部署多签合约

msg-chain-devkit tx wasm store cw3_fixed_multisig.wasm \
  --from signer1 --gas auto --gas-adjustment 1.3

msg-chain-devkit query wasm list-code

INIT='{
  "signers": [
    {"addr": "msg1signer1...", "weight": "1"},
    {"addr": "msg1signer2...", "weight": "1"},
    {"addr": "msg1signer3...", "weight": "1"},
    {"addr": "msg1signer4...", "weight": "1"},
    {"addr": "msg1signer5...", "weight": "1"}
  ],
  "threshold": {"absolute_count": {"weight": 3}},
  "max_voting_period": {"time": 604800}
}'

msg-chain-devkit tx wasm instantiate CODE_ID "$INIT" \
  --from signer1 --label "team-multisig-3-5" --admin msg1admin...

4.2 Python SDK 操作

#!/usr/bin/env python3
"""
MSG Chain 多签钱包操作示例
"""

import json
import time
from typing import List, Optional
from dataclasses import dataclass
from msg_chain_sdk import (
    MsgChainClient,
    Wallet,
    Coin,
)

CHAIN_ID = "msg-chain-1"
RPC_ENDPOINT = "https://rpc.msgchain.org"
MULTISIG_CONTRACT = "msg1multisigcontractaddress..."
GAS_PRICE = 1_000_000_000
GAS_LIMIT = 500_000


@dataclass
class Proposal:
    id: int
    title: str
    description: str
    status: str
    yes_weight: int
    no_weight: int
    total_weight: int
    expires: int
    proposer: str


@dataclass
class VoterInfo:
    address: str
    weight: int


class MultisigClient:
    """多签钱包客户端"""

    def __init__(self, rpc: str, chain_id: str, contract_addr: str, gas_price: int = 1_000_000_000):
        self.client = MsgChainClient(rpc, chain_id)
        self.contract_addr = contract_addr
        self.gas_price = gas_price

    def propose(self, wallet: Wallet, title: str, description: str,
                msgs: List[dict], earliest: Optional[int] = None) -> int:
        propose_msg = {"propose": {"title": title, "description": description, "msgs": msgs}}
        if earliest is not None:
            propose_msg["propose"]["earliest"] = {"at_time": str(earliest)}

        result = self.client.execute_contract(
            wallet=wallet, contract=self.contract_addr, msg=propose_msg,
            gas_limit=GAS_LIMIT, gas_price=str(self.gas_price),
        )
        proposal_id = self._parse_proposal_id(result)
        print(f"[✓] 提案 #{proposal_id} 已创建: {title}")
        return proposal_id

    def vote(self, wallet: Wallet, proposal_id: int, vote_type: str) -> bool:
        vote_msg = {"vote": {"proposal_id": proposal_id, "vote": vote_type}}
        result = self.client.execute_contract(
            wallet=wallet, contract=self.contract_addr, msg=vote_msg,
            gas_limit=200_000, gas_price=str(self.gas_price),
        )
        print(f"[✓] {wallet.address()} 对提案 #{proposal_id} 投了 {vote_type}")
        return True

    def execute(self, wallet: Wallet, proposal_id: int) -> bool:
        execute_msg = {"execute": {"proposal_id": proposal_id}}
        result = self.client.execute_contract(
            wallet=wallet, contract=self.contract_addr, msg=execute_msg,
            gas_limit=GAS_LIMIT, gas_price=str(self.gas_price),
        )
        print(f"[✓] 提案 #{proposal_id} 已执行")
        return True

    def close(self, wallet: Wallet, proposal_id: int) -> bool:
        close_msg = {"close": {"proposal_id": proposal_id}}
        result = self.client.execute_contract(
            wallet=wallet, contract=self.contract_addr, msg=close_msg,
            gas_limit=200_000, gas_price=str(self.gas_price),
        )
        print(f"[✓] 提案 #{proposal_id} 已关闭")
        return True

    def get_proposal(self, proposal_id: int) -> Optional[Proposal]:
        query_msg = {"proposal": {"proposal_id": proposal_id}}
        result = self.client.query_contract(self.contract_addr, query_msg)
        if "error" in result:
            print(f"[✗] 查询失败: {result['error']}")
            return None
        prop = result["data"]
        return Proposal(
            id=prop["id"], title=prop["title"], description=prop["description"],
            status=prop["status"], yes_weight=int(prop["yes_weight"]),
            no_weight=int(prop["no_weight"]), total_weight=int(prop["total_weight"]),
            expires=prop["expires"], proposer=prop["proposer"],
        )

    def get_voters(self) -> List[VoterInfo]:
        query_msg = {"list_voters": {}}
        result = self.client.query_contract(self.contract_addr, query_msg)
        voters = []
        for v in result["data"]["voters"]:
            voters.append(VoterInfo(address=v["addr"], weight=int(v["weight"])))
        return voters

    def get_config(self) -> dict:
        query_msg = {"config": {}}
        result = self.client.query_contract(self.contract_addr, query_msg)
        return result["data"]

    def _parse_proposal_id(self, result: dict) -> int:
        for event in result.get("events", []):
            for attr in event.get("attributes", []):
                if attr.get("key") == "proposal_id":
                    return int(attr["value"])
        raise ValueError("无法解析 proposal_id")


def demo_multisig_workflow():
    client = MultisigClient(RPC_ENDPOINT, CHAIN_ID, MULTISIG_CONTRACT)
    signer1 = Wallet.from_mnemonic("mnemonic phrase for signer 1...")
    signer2 = Wallet.from_mnemonic("mnemonic phrase for signer 2...")
    signer3 = Wallet.from_mnemonic("mnemonic phrase for signer 3...")

    config = client.get_config()
    print(f"合约配置: {config}")

    voters = client.get_voters()
    print(f"签名者 ({len(voters)}): {voters}")

    print("\n--- 创建转账提案 ---")
    transfer_msg = {
        "bank": {
            "send": {
                "to_address": "msg1recipientaddress...",
                "amount": [{"denom": "umsg", "amount": "1000000000000000000"}]
            }
        }
    }

    proposal_id = client.propose(
        wallet=signer1, title="支付 Q1 开发者资助",
        description="向 msg1recipient 转账 1 MSG",
        msgs=[transfer_msg],
    )

    print("\n--- 投票 ---")
    client.vote(signer2, proposal_id, "yes")
    proposal = client.get_proposal(proposal_id)
    print(f"当前状态: {proposal.status}, 赞成: {proposal.yes_weight}/{proposal.total_weight}")

    client.vote(signer3, proposal_id, "yes")
    proposal = client.get_proposal(proposal_id)
    print(f"投票后状态: {proposal.status}")

    if proposal.status == "passed":
        print("\n--- 执行提案 ---")
        client.execute(signer1, proposal_id)
        proposal = client.get_proposal(proposal_id)
        print(f"最终状态: {proposal.status}")

    return proposal_id


def create_multisig_transfer(client: MultisigClient, proposer: Wallet,
                              recipients: List[tuple[str, int]],
                              title: str = "批量转账", description: str = "") -> int:
    msgs = []
    for addr, amount in recipients:
        msgs.append({
            "bank": {
                "send": {
                    "to_address": addr,
                    "amount": [{"denom": "umsg", "amount": str(amount)}],
                }
            }
        })
    return client.propose(wallet=proposer, title=title,
                          description=description or f"向 {len(recipients)} 个地址批量转账",
                          msgs=msgs)


def create_contract_exec_multisig(client: MultisigClient, proposer: Wallet,
                                   contract_addr: str, exec_msg: dict,
                                   funds: Optional[List[dict]] = None,
                                   title: str = "合约调用", description: str = "") -> int:
    wasm_msg = {
        "wasm": {
            "execute": {
                "contract_addr": contract_addr,
                "msg": exec_msg,
                "funds": funds or [],
            }
        }
    }
    return client.propose(wallet=proposer, title=title,
                          description=description or f"调用合约 {contract_addr}",
                          msgs=[wasm_msg])


def monitor_proposal_status(client: MultisigClient, proposal_id: int,
                             interval: int = 5, timeout: int = 300) -> str:
    start = time.time()
    while time.time() - start < timeout:
        proposal = client.get_proposal(proposal_id)
        if proposal is None:
            break
        print(f"[{time.time() - start:.0f}s] 提案 #{proposal_id} 状态: {proposal.status}")
        if proposal.status in ("passed", "rejected", "executed", "closed", "expired"):
            return proposal.status
        time.sleep(interval)
    return "timeout"

4.3 TypeScript SDK 操作

// msig-operations.ts
import {
  MsgChainClient,
  Wallet,
  MsgExecuteContract,
  calculateFee,
  GasPrice,
} from "@msg-chain/sdk";

const CHAIN_ID = "msg-chain-1";
const RPC_ENDPOINT = "https://rpc.msgchain.org";
const CONTRACT_ADDR = "msg1multisigcontract...";
const GAS_PRICE = GasPrice.fromString("1000000000attoMSG");

interface Voter {
  addr: string;
  weight: string;
}

interface Proposal {
  id: number;
  title: string;
  description: string;
  status: string;
  yes_weight: string;
  no_weight: string;
  veto_weight: string;
  abstain_weight: string;
  total_weight: string;
  expires: any;
  proposer: string;
}

interface Config {
  threshold: any;
  max_voting_period: any;
  total_weight: string;
}

class MultisigClient {
  private client: MsgChainClient;
  private contractAddr: string;

  constructor(rpc: string, chainId: string, contractAddr: string) {
    this.client = new MsgChainClient({ rpc, chainId });
    this.contractAddr = contractAddr;
  }

  async propose(
    wallet: Wallet, title: string, description: string,
    msgs: object[], earliest?: number
  ): Promise<number> {
    const proposeMsg: MsgExecuteContract = {
      typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
      value: {
        sender: wallet.address,
        contract: this.contractAddr,
        msg: Buffer.from(JSON.stringify({
          propose: { title, description, msgs,
            ...(earliest ? { earliest: { at_time: earliest.toString() } } : {}) },
        })).toString("base64"),
        funds: [],
      },
    };
    const fee = calculateFee(500_000, GAS_PRICE);
    const result = await this.client.signAndBroadcast(wallet, [proposeMsg], fee);
    const proposalId = this.parseProposalId(result);
    console.log(`[✓] 提案 #${proposalId} 已创建: ${title}`);
    return proposalId;
  }

  async vote(
    wallet: Wallet, proposalId: number, voteType: "yes" | "no" | "veto" | "abstain"
  ): Promise<boolean> {
    const voteMsg: MsgExecuteContract = {
      typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
      value: {
        sender: wallet.address, contract: this.contractAddr,
        msg: Buffer.from(JSON.stringify({
          vote: { proposal_id: proposalId, vote: voteType },
        })).toString("base64"),
        funds: [],
      },
    };
    const fee = calculateFee(200_000, GAS_PRICE);
    await this.client.signAndBroadcast(wallet, [voteMsg], fee);
    console.log(`[✓] ${wallet.address} 对提案 #${proposalId} 投了 ${voteType}`);
    return true;
  }

  async execute(wallet: Wallet, proposalId: number): Promise<boolean> {
    const executeMsg: MsgExecuteContract = {
      typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
      value: {
        sender: wallet.address, contract: this.contractAddr,
        msg: Buffer.from(JSON.stringify({
          execute: { proposal_id: proposalId },
        })).toString("base64"),
        funds: [],
      },
    };
    const fee = calculateFee(500_000, GAS_PRICE);
    await this.client.signAndBroadcast(wallet, [executeMsg], fee);
    console.log(`[✓] 提案 #${proposalId} 已执行`);
    return true;
  }

  async close(wallet: Wallet, proposalId: number): Promise<boolean> {
    const closeMsg: MsgExecuteContract = {
      typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
      value: {
        sender: wallet.address, contract: this.contractAddr,
        msg: Buffer.from(JSON.stringify({
          close: { proposal_id: proposalId },
        })).toString("base64"),
        funds: [],
      },
    };
    const fee = calculateFee(200_000, GAS_PRICE);
    await this.client.signAndBroadcast(wallet, [closeMsg], fee);
    console.log(`[✓] 提案 #${proposalId} 已关闭`);
    return true;
  }

  async getProposal(proposalId: number): Promise<Proposal | null> {
    const result: any = await this.client.queryContractSmart(
      this.contractAddr, { proposal: { proposal_id: proposalId } }
    );
    return result as Proposal;
  }

  async getVoters(): Promise<Voter[]> {
    const result: any = await this.client.queryContractSmart(
      this.contractAddr, { list_voters: {} }
    );
    return result.voters;
  }

  async getConfig(): Promise<Config> {
    const result: any = await this.client.queryContractSmart(
      this.contractAddr, { config: {} }
    );
    return result;
  }

  async listProposals(): Promise<Proposal[]> {
    const result: any = await this.client.queryContractSmart(
      this.contractAddr, { list_proposals: {} }
    );
    return result.proposals || [];
  }

  private parseProposalId(result: any): number {
    for (const event of result.events || []) {
      for (const attr of event.attributes || []) {
        if (attr.key === "proposal_id") return parseInt(attr.value, 10);
      }
    }
    throw new Error("无法解析 proposal_id");
  }
}

async function main() {
  const client = new MultisigClient(RPC_ENDPOINT, CHAIN_ID, CONTRACT_ADDR);
  const signer1 = await Wallet.fromMnemonic("mnemonic phrase...");
  const signer2 = await Wallet.fromMnemonic("mnemonic phrase...");
  const signer3 = await Wallet.fromMnemonic("mnemonic phrase...");

  const config = await client.getConfig();
  console.log("合约配置:", config);

  const voters = await client.getVoters();
  console.log(`签名者 (${voters.length}):`, voters);

  console.log("\n--- 创建提案 ---");
  const proposalId = await client.propose(
    signer1, "向社区金库充值", "转入 100 MSG",
    [{
      bank: {
        send: {
          to_address: "msg1communitypool...",
          amount: [{ denom: "umsg", amount: "100000000000000000000" }],
        },
      },
    }]
  );

  console.log("\n--- 投票 ---");
  await client.vote(signer2, proposalId, "yes");
  let proposal = await client.getProposal(proposalId);
  console.log(`状态: ${proposal!.status}, 赞成: ${proposal!.yes_weight}/${proposal!.total_weight}`);

  await client.vote(signer3, proposalId, "yes");
  proposal = await client.getProposal(proposalId);
  console.log(`投票后状态: ${proposal!.status}`);

  if (proposal!.status === "passed") {
    console.log("\n--- 执行提案 ---");
    await client.execute(signer1, proposalId);
    proposal = await client.getProposal(proposalId);
    console.log(`最终状态: ${proposal!.status}`);
  }
}

main().catch(console.error);

4.4 命令行完整流程

#!/bin/bash

CHAIN_ID="msg-chain-1"
NODE="https://rpc.msgchain.org"
MULTISIG="msg1multisigcontract..."
SIGNER1="signer1"
SIGNER2="signer2"
SIGNER3="signer3"

echo "=== 查询合约配置 ==="
msg-chain-devkit query wasm contract-state smart "$MULTISIG" \
  '{"config":{}}' --node "$NODE" --chain-id "$CHAIN_ID"

echo "=== 查询投票者 ==="
msg-chain-devkit query wasm contract-state smart "$MULTISIG" \
  '{"list_voters":{}}' --node "$NODE" --chain-id "$CHAIN_ID"

echo "=== 创建转账提案 ==="
PROPOSAL_RESULT=$(msg-chain-devkit tx wasm execute "$MULTISIG" \
  '{
    "propose": {
      "title": "开发者资助转账",
      "description": "向开发者转账 10 MSG",
      "msgs": [{
        "bank": {
          "send": {
            "to_address": "msg1developer...",
            "amount": [{"denom": "umsg", "amount": "10000000"}]
          }
        }
      }]
    }
  }' \
  --from "$SIGNER1" --gas auto --gas-adjustment 1.4 \
  --node "$NODE" --chain-id "$CHAIN_ID" -o json)

echo "提案结果: $PROPOSAL_RESULT"
PROPOSAL_ID=$(echo "$PROPOSAL_RESULT" | jq -r '.raw_log' | grep -oP 'proposal_id: \K\d+')
echo "提案 ID: $PROPOSAL_ID"

echo "=== 签名者 2 投票 ==="
msg-chain-devkit tx wasm execute "$MULTISIG" \
  "{\"vote\":{\"proposal_id\":$PROPOSAL_ID,\"vote\":\"yes\"}}" \
  --from "$SIGNER2" --gas auto --gas-adjustment 1.3 \
  --node "$NODE" --chain-id "$CHAIN_ID"

echo "=== 签名者 3 投票 ==="
msg-chain-devkit tx wasm execute "$MULTISIG" \
  "{\"vote\":{\"proposal_id\":$PROPOSAL_ID,\"vote\":\"yes\"}}" \
  --from "$SIGNER3" --gas auto --gas-adjustment 1.3 \
  --node "$NODE" --chain-id "$CHAIN_ID"

echo "=== 提案状态 ==="
msg-chain-devkit query wasm contract-state smart "$MULTISIG" \
  "{\"proposal\":{\"proposal_id\":$PROPOSAL_ID}}" \
  --node "$NODE" --chain-id "$CHAIN_ID" | jq

echo "=== 执行提案 ==="
msg-chain-devkit tx wasm execute "$MULTISIG" \
  "{\"execute\":{\"proposal_id\":$PROPOSAL_ID}}" \
  --from "$SIGNER1" --gas auto --gas-adjustment 1.5 \
  --node "$NODE" --chain-id "$CHAIN_ID"

echo "=== 验证结果 ==="
msg-chain-devkit query wasm contract-state smart "$MULTISIG" \
  "{\"proposal\":{\"proposal_id\":$PROPOSAL_ID}}" \
  --node "$NODE" --chain-id "$CHAIN_ID" | jq '.data.status'

4.5 提案生命周期状态机

                    ┌─────────────┐
                    │   Pending   │
                    └──────┬──────┘
                           │ (提案人自动投 Yes)
                           ▼
                    ┌─────────────┐
         ┌─────────│    Open     │──────────┐
         │         └──────┬──────┘          │
         │                │                 │
         ▼                ▼                 ▼
   ┌──────────┐   ┌──────────────┐   ┌──────────┐
   │ Expired  │   │   Passed     │   │ Rejected │
   └────┬─────┘   └──────┬───────┘   └────┬─────┘
        │                │                │
        ▼                ▼                ▼
   ┌──────────┐   ┌──────────────┐   ┌──────────┐
   │  Closed  │   │  Executed    │   │  Closed  │
   └──────────┘   └──────────────┘   └──────────┘

5. 前端组件

5.1 React 多签组件

// MultisigDashboard.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { MsgChainClient, Wallet } from '@msg-chain/sdk';

interface MultisigDashboardProps {
  rpcEndpoint: string;
  chainId: string;
  contractAddr: string;
  userAddress: string;
}

type ProposalFilter = 'all' | 'open' | 'passed' | 'executed' | 'rejected';
type VoteType = 'yes' | 'no' | 'veto' | 'abstain';

interface Voter {
  addr: string;
  weight: string;
}

interface Proposal {
  id: number;
  title: string;
  description: string;
  status: string;
  yes_weight: string;
  no_weight: string;
  veto_weight: string;
  abstain_weight: string;
  total_weight: string;
  expires: any;
  proposer: string;
}

interface Config {
  threshold: any;
  max_voting_period: any;
  total_weight: string;
}

const filterLabels: Record<ProposalFilter, string> = {
  all: '全部', open: '投票中', passed: '已通过',
  executed: '已执行', rejected: '已拒绝',
};

function shortenAddress(addr: string, chars: number = 10): string {
  if (addr.length <= chars * 2) return addr;
  return `${addr.slice(0, chars)}...${addr.slice(-chars)}`;
}

const ProposalCard: React.FC<{
  proposal: Proposal;
  onVote: (id: number, vote: VoteType) => void;
  onExecute: (id: number) => void;
  onClose: (id: number) => void;
}> = ({ proposal, onVote, onExecute, onClose }) => {
  const progress = proposal.total_weight !== '0'
    ? (parseInt(proposal.yes_weight) / parseInt(proposal.total_weight)) * 100
    : 0;

  const statusLabels: Record<string, string> = {
    open: '投票中', passed: '已通过', rejected: '已拒绝',
    executed: '已执行', closed: '已关闭', expired: '已过期',
  };

  return (
    <div className={`proposal-card status-${proposal.status}`}>
      <div className="proposal-header">
        <span className="proposal-id">#{proposal.id}</span>
        <span className="proposal-status">{statusLabels[proposal.status] || proposal.status}</span>
      </div>
      <h3 className="proposal-title">{proposal.title}</h3>
      <p className="proposal-description">{proposal.description}</p>
      <div className="proposal-meta">提案人: {shortenAddress(proposal.proposer)}</div>
      <div className="vote-progress">
        <div className="progress-bar">
          <div className="progress-fill" style={{ width: `${Math.min(progress, 100)}%` }} />
        </div>
        <div className="vote-stats">
          <span className="yes">赞成: {proposal.yes_weight}</span>
          <span className="no">反对: {proposal.no_weight}</span>
          <span className="abstain">弃权: {proposal.abstain_weight}</span>
        </div>
      </div>
      {proposal.status === 'open' && (
        <div className="vote-actions">
          <button onClick={() => onVote(proposal.id, 'yes')} className="btn-yes">赞成</button>
          <button onClick={() => onVote(proposal.id, 'no')} className="btn-no">反对</button>
          <button onClick={() => onVote(proposal.id, 'veto')} className="btn-veto">强烈反对</button>
          <button onClick={() => onVote(proposal.id, 'abstain')} className="btn-abstain">弃权</button>
        </div>
      )}
      {proposal.status === 'passed' && (
        <button onClick={() => onExecute(proposal.id)} className="btn-execute">执行提案</button>
      )}
      {proposal.status === 'expired' && (
        <button onClick={() => onClose(proposal.id)} className="btn-close">关闭提案</button>
      )}
    </div>
  );
};

const VoterManager: React.FC<{
  voters: Voter[]; config: Config; isOwner: boolean;
  onAddVoter?: (addr: string, weight: string) => void;
  onRemoveVoter?: (addr: string) => void;
}> = ({ voters, config, isOwner, onAddVoter, onRemoveVoter }) => {
  const [showForm, setShowForm] = useState(false);
  const [newAddr, setNewAddr] = useState('');
  const [newWeight, setNewWeight] = useState('1');

  return (
    <div className="voter-manager">
      <div className="voter-manager-header">
        <h3>签名者</h3>
        <span>{voters.length} 人 (总权重 {config.total_weight})</span>
        {isOwner && <button onClick={() => setShowForm(!showForm)}>+ 添加</button>}
      </div>
      {showForm && (
        <div className="add-voter-form">
          <input value={newAddr} onChange={e => setNewAddr(e.target.value)} placeholder="msg1..." />
          <input value={newWeight} onChange={e => setNewWeight(e.target.value)} placeholder="权重" />
          <button onClick={() => { onAddVoter?.(newAddr, newWeight); setShowForm(false); }}>确认</button>
        </div>
      )}
      {voters.map(v => (
        <div key={v.addr} className="voter-row">
          <span>{shortenAddress(v.addr)}</span>
          <span>{v.weight}</span>
          {isOwner && <button onClick={() => onRemoveVoter?.(v.addr)}>✕</button>}
        </div>
      ))}
    </div>
  );
};

const ProposeForm: React.FC<{
  onSubmit: (title: string, desc: string, msgs: object[]) => void;
  onCancel: () => void;
}> = ({ onSubmit, onCancel }) => {
  const [title, setTitle] = useState('');
  const [desc, setDesc] = useState('');
  const [recipient, setRecipient] = useState('');
  const [amount, setAmount] = useState('');

  return (
    <div className="propose-form-overlay">
      <div className="propose-form-modal">
        <h2>创建提案</h2>
        <form onSubmit={e => {
          e.preventDefault();
          onSubmit(title, desc, [{
            bank: { send: { to_address: recipient, amount: [{ denom: 'umsg', amount }] } },
          }]);
        }}>
          <label>标题</label>
          <input value={title} onChange={e => setTitle(e.target.value)} required />
          <label>描述</label>
          <textarea value={desc} onChange={e => setDesc(e.target.value)} rows={3} />
          <label>收款地址</label>
          <input value={recipient} onChange={e => setRecipient(e.target.value)} placeholder="msg1..." required />
          <label>金额 (umsg)</label>
          <input value={amount} onChange={e => setAmount(e.target.value)} required />
          <div className="form-actions">
            <button type="button" onClick={onCancel}>取消</button>
            <button type="submit">提交</button>
          </div>
        </form>
      </div>
    </div>
  );
};

export const MultisigDashboard: React.FC<MultisigDashboardProps> = ({
  rpcEndpoint, chainId, contractAddr, userAddress,
}) => {
  const [client, setClient] = useState<any>(null);
  const [config, setConfig] = useState<Config | null>(null);
  const [voters, setVoters] = useState<Voter[]>([]);
  const [proposals, setProposals] = useState<Proposal[]>([]);
  const [filter, setFilter] = useState<ProposalFilter>('all');
  const [showForm, setShowForm] = useState(false);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const init = async () => {
      const c = { contractAddr, rpcEndpoint, chainId } as any;
      setClient(c);
      try {
        const [cfg, vrs, props] = await Promise.all([
          fetch(`https://rest.msgchain.org/cosmwasm/wasm/v1/contract/${contractAddr}/smart`,
            { method: 'POST', body: JSON.stringify({ data: Buffer.from(JSON.stringify({ config: {} })).toString('base64') }) }),
          // In production, use proper SDK client
        ]);
      } finally { setLoading(false); }
    };
    init();
  }, []);

  const handlePropose = useCallback(async (title: string, desc: string, msgs: object[]) => {
    console.log('Propose:', title, desc, msgs);
    setShowForm(false);
  }, []);

  const handleVote = useCallback(async (id: number, vote: VoteType) => {
    console.log('Vote:', id, vote);
  }, []);

  const filteredProposals = proposals.filter(p => filter === 'all' || p.status === filter);

  if (loading) return <div>加载多签钱包...</div>;

  return (
    <div className="multisig-dashboard">
      <div className="dashboard-header">
        <h1>多签钱包</h1>
        <span>合约: {shortenAddress(contractAddr)}</span>
        <button onClick={() => setShowForm(true)}>+ 创建提案</button>
      </div>
      <div className="dashboard-grid">
        <VoterManager voters={voters} config={config!} isOwner={false} />
        <div>
          <div className="filter-tabs">
            {(Object.keys(filterLabels) as ProposalFilter[]).map(f => (
              <button key={f} className={`filter-tab ${filter === f ? 'active' : ''}`}
                onClick={() => setFilter(f)}>{filterLabels[f]}</button>
            ))}
          </div>
          {filteredProposals.map(p => (
            <ProposalCard key={p.id} proposal={p}
              onVote={handleVote} onExecute={handleVote} onClose={handleVote} />
          ))}
        </div>
      </div>
      {showForm && <ProposeForm onSubmit={handlePropose} onCancel={() => setShowForm(false)} />}
    </div>
  );
};

5.2 React 组件样式

.multisig-dashboard {
  max-width: 1200px; margin: 0 auto; padding: 24px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.dashboard-header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid #e0e0e0;
}
.dashboard-header h1 { font-size: 24px; margin: 0; }
.dashboard-grid { display: grid; grid-template-columns: 320px 1fr; gap: 24px; }

.voter-manager {
  background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px;
}
.voter-manager-header {
  display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;
}
.voter-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 6px 0; border-bottom: 1px solid #f0f0f0; font-size: 13px;
}
.voter-addr { font-family: monospace; color: #374151; }

.proposal-card {
  background: white; border: 1px solid #e5e7eb; border-radius: 8px;
  padding: 16px; margin-bottom: 12px;
}
.proposal-card.status-passed { border-left: 3px solid #10b981; }
.proposal-card.status-rejected { border-left: 3px solid #ef4444; }

.proposal-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
.proposal-title { margin: 0 0 4px; font-size: 16px; font-weight: 500; }
.proposal-description { font-size: 13px; color: #6b7280; margin-bottom: 8px; }
.progress-bar { height: 6px; background: #e5e7eb; border-radius: 3px; overflow: hidden; }
.progress-fill { height: 100%; background: #10b981; transition: width 0.3s; }
.vote-stats { display: flex; gap: 16px; font-size: 12px; }
.vote-stats .yes { color: #10b981; }
.vote-stats .no { color: #ef4444; }

.vote-actions { display: flex; gap: 8px; }
.btn-yes { background: #ecfdf5; color: #059669; border: 1px solid #a7f3d0; }
.btn-no { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; }
.btn-veto { background: #fff7ed; color: #ea580c; border: 1px solid #fed7aa; }
.btn-abstain { background: #f9fafb; color: #6b7280; border: 1px solid #e5e7eb; }
.btn-execute { background: #059669; color: white; border: none; padding: 8px 20px; border-radius: 6px; }
button { cursor: pointer; padding: 6px 14px; border-radius: 6px; font-size: 13px; }

.filter-tabs { display: flex; gap: 4px; margin-bottom: 16px; }
.filter-tab { border: 1px solid #e5e7eb; background: transparent; color: #374151; }
.filter-tab.active { background: #4f46e5; color: white; border-color: #4f46e5; }

.propose-form-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.4);
  display: flex; align-items: center; justify-content: center; z-index: 100;
}
.propose-form-modal {
  background: white; border-radius: 12px; padding: 32px; max-width: 500px; width: 100%;
}
.form-actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 16px; }

6. 安全最佳实践

6.1 阈值选择

合理的阈值设置是多签安全性的核心:

签名者数 推荐阈值 安全级别 说明
3 2/3 基础 适合小额运营资金
5 3/5 标准 推荐,兼顾安全与可用性
7 4/7 较高 适合大额资金管理
9 5/9 高 适合协议金库

原则:

6.2 签名者密钥管理

// 密钥轮换建议
// 1. 使用硬件钱包存储签名者密钥
// 2. 实施社会恢复机制
// 3. 定期轮换密钥(每季度)

pub fn rotate_key_checks() -> Vec<String> {
    vec![
        "每个签名者使用独立的密钥".to_string(),
        "密钥保存在硬件钱包中(Ledger/Trezor)".to_string(),
        "备份助记词并分散存储在安全地点".to_string(),
        "至少两个签名者不在同一地理位置".to_string(),
        "使用不同的密钥派生路径".to_string(),
    ]
}

6.3 提案消息验证

// 提案消息安全检查
pub fn validate_proposal_msgs(msgs: &[CosmosMsg<Empty>]) -> StdResult<()> {
    for msg in msgs {
        match msg {
            CosmosMsg::Bank(bank_msg) => {
                match bank_msg {
                    BankMsg::Send { to_address, amount } => {
                        // 验证地址格式
                        if !to_address.starts_with("msg1") {
                            return Err(StdError::generic_err("Invalid recipient address"));
                        }
                        // 验证金额不为零
                        for coin in amount {
                            if coin.amount.is_zero() {
                                return Err(StdError::generic_err("Zero amount not allowed"));
                            }
                        }
                    }
                    _ => return Err(StdError::generic_err("Unsupported bank message type")),
                }
            }
            CosmosMsg::Wasm(wasm_msg) => {
                match wasm_msg {
                    WasmMsg::Execute { contract_addr, .. } => {
                        // 验证合约地址
                        if !contract_addr.starts_with("msg1") {
                            return Err(StdError::generic_err("Invalid contract address"));
                        }
                    }
                    WasmMsg::Migrate { .. } => {
                        // 迁移操作需要额外安全检查
                        return Err(StdError::generic_err(
                            "Migration requires additional authorization",
                        ));
                    }
                    _ => {}
                }
            }
            _ => {
                return Err(StdError::generic_err("Unsupported message type in proposal"));
            }
        }
    }
    Ok(())
}

6.4 紧急暂停机制

// 紧急暂停状态
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct EmergencyState {
    pub paused: bool,
    pub pause_admin: Addr,
    pub unpause_threshold: Threshold,
    pub pause_reason: Option<String>,
}

pub const EMERGENCY: Item<EmergencyState> = Item::new("emergency");

pub fn execute_emergency_pause(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    reason: String,
) -> StdResult<Response> {
    let state = EMERGENCY.load(deps.storage)?;
    if info.sender != state.pause_admin {
        return Err(StdError::generic_err("Unauthorized: not pause admin"));
    }

    EMERGENCY.save(deps.storage, &EmergencyState {
        paused: true,
        pause_admin: state.pause_admin,
        unpause_threshold: state.unpause_threshold,
        pause_reason: Some(reason),
    })?;

    Ok(Response::new()
        .add_attribute("method", "emergency_pause")
        .add_attribute("reason", reason))
}

pub fn execute_emergency_unpause(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
) -> StdResult<Response> {
    let state = EMERGENCY.load(deps.storage)?;
    if !state.paused {
        return Err(StdError::generic_err("Contract is not paused"));
    }

    // 仅 pause_admin 可解暂停,或通过多签投票
    if info.sender != state.pause_admin {
        return Err(StdError::generic_err("Unauthorized: not pause admin"));
    }

    EMERGENCY.save(deps.storage, &EmergencyState {
        paused: false,
        ..state
    })?;

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

6.5 安全清单

/// 多签合约安全清单
/// 
/// [x] 防止重复投票
/// [x] 验证提案者身份
/// [x] 过期提案自动失效
/// [x] 已执行提案不可重复执行
/// [x] 消息数量限制
/// [x] 描述长度限制
/// [x] 阈值校验
/// [ ] 时间锁
/// [ ] 紧急暂停
/// [ ] 提案消息白名单
/// [ ] 签名者数量下限
/// [ ] Gas 上限控制
/// [ ] 重入防护
/// [ ] 事件日志完善

部署前检查清单:

  1. 所有签名者地址确认正确(建议在测试网先验证)
  2. 阈值设置合理(不低于 50% + 1)
  3. 投票期设置合理(7 天为推荐值)
  4. 合约管理员设置正确
  5. 测试多签全部流程(提案 - 投票 - 执行 - 关闭)
  6. 确认所有签名者都能正常连接 MSG Chain
  7. 备份合约实例化参数

7. 集成场景

7.1 DAO 财库管理

// DAO 治理 + 多签财库集成
//
// 工作流程:
// 1. DAO 成员投票通过治理提案
// 2. DAO 治理合约调用多签财库的 Propose
// 3. 多签签名者审核并投票
// 4. 达到阈值后执行

use cosmwasm_std::CosmosMsg;

// DAO 治理合约 -> 多签财库 的调用示例
pub fn create_dao_treasury_proposal(
    dao_contract: &Addr,
    multisig_contract: &Addr,
    to_address: &str,
    amount: Uint128,
) -> CosmosMsg {
    CosmosMsg::Wasm(WasmMsg::Execute {
        contract_addr: multisig_contract.to_string(),
        msg: to_binary(&ExecuteMsg::Propose {
            title: "DAO 治理执行:资金拨付".to_string(),
            description: format!(
                "根据 DAO 提案 #{} 执行资金拨付",
                "PROPOSAL_ID"
            ),
            msgs: vec![CosmosMsg::Bank(BankMsg::Send {
                to_address: to_address.to_string(),
                amount: coins(amount.u128(), "umsg"),
            })],
            earliest: None,
        })?,
        funds: vec![],
    })
}

DAO 财库合约示例

// dao_treasury.rs - DAO 财库多签封装
use cosmwasm_std::{
    entry_point, to_binary, Binary, CosmosMsg, Deps, DepsMut, Env,
    MessageInfo, Response, StdResult, WasmMsg,
};

pub struct DAOTreasuryConfig {
    pub governance_addr: Addr,
    pub multisig_addr: Addr,
    pub min_delay: Duration,
}

pub fn create_treasury_proposal(
    deps: DepsMut,
    _env: Env,
    info: MessageInfo,
    msgs: Vec<CosmosMsg>,
    title: String,
    description: String,
) -> StdResult<Response> {
    // 验证调用者是否为 DAO 治理合约
    let config = DAO_TREASURY_CONFIG.load(deps.storage)?;
    if info.sender != config.governance_addr {
        return Err(StdError::generic_err("Unauthorized: only governance contract can propose"));
    }

    // 通过多签合约创建提案
    let propose_msg = CosmosMsg::Wasm(WasmMsg::Execute {
        contract_addr: config.multisig_addr.to_string(),
        msg: to_binary(&ExecuteMsg::Propose {
            title,
            description,
            msgs,
            earliest: None,
        })?,
        funds: vec![],
    });

    Ok(Response::new()
        .add_message(propose_msg)
        .add_attribute("method", "create_treasury_proposal"))
}

7.2 跨合约多签调用

// 多签跨合约调用示例

interface CrossContractCall {
  contractAddr: string;
  msg: object;
  funds?: Coin[];
}

async function multisigContractCall(
  client: MultisigClient,
  wallet: Wallet,
  calls: CrossContractCall[],
  title: string,
  description: string,
) {
  const msgs = calls.map(call => ({
    wasm: {
      execute: {
        contract_addr: call.contractAddr,
        msg: Buffer.from(JSON.stringify(call.msg)).toString("base64"),
        funds: call.funds || [],
      },
    },
  }));

  return client.propose(wallet, title, description, msgs);
}

// 示例:同时调用多个合约
const batchCalls = [
  {
    contractAddr: "msg1stakingcontract...",
    msg: { delegate: { validator: "msgvaloper1...", amount: "1000000" } },
  },
  {
    contractAddr: "msg1lendingcontract...",
    msg: { deposit: { amount: "5000000" } },
  },
];

await multisigContractCall(
  client, wallet, batchCalls,
  "批量 DeFi 操作",
  "同时执行质押和借贷存款操作"
);

7.3 集成宪章引擎

// 宪章引擎 + 多签集成
// 某些高价值操作需要宪章规则触发多签验证

pub enum ConstitutionAction {
    TreasuryWithdrawal {
        amount: Uint128,
        reason: String,
    },
    ContractUpgrade {
        code_id: u64,
        reason: String,
    },
    EmergencyAction {
        action_type: String,
        parameters: Binary,
    },
}

pub fn constitution_multisig_check(
    action: &ConstitutionAction,
    constitution: &Constitution,
) -> StdResult<bool> {
    match action {
        ConstitutionAction::TreasuryWithdrawal { amount, .. } => {
            // 小额:2/3 多签
            if amount < Uint128::new(1_000_000_000_000_000_000u128) { // 1000 MSG
                return Ok(true); // 标准多签阈值
            }
            // 大额:4/5 多签 + 时间锁 7 天
            return Ok(false); // 需要更严格的配置
        }
        ConstitutionAction::ContractUpgrade { .. } => {
            // 合约升级需要较高阈值
            return Ok(false);
        }
        ConstitutionAction::EmergencyAction { .. } => {
            // 紧急操作可以降低阈值
            return Ok(true);
        }
    }
}

7.4 Agent MPC 钱包对比与选择

维度 CW3 多签 Agent MPC 钱包 推荐场景
部署成本 高(合约开发部署) 低(直接使用 SDK) MPC 适合快速启动
透明度 完全链上透明 签名过程不公开 合规需求选 CW3
Gas 成本 O(n) 投票交易 单笔交易 高频操作选 MPC
签名者管理 合约内管理 配置化管理 需要审计选 CW3
可编程性 完全可编程 策略引擎受限 复杂逻辑选 CW3
恢复机制 合约内实现 外部配置 安全要求高选 CW3

选择建议:

7.5 混合架构:CW3 + MPC

// 混合多签架构
// 采用 CW3 作为最终执行层,MPC 作为签名聚合层

pub struct HybridMultisigConfig {
    pub cw3_contract: Addr,        // CW3 合约地址
    pub mpc_agent: Addr,           // MPC Agent 地址
    pub mpc_threshold: u32,        // MPC 组阈值
    pub mpc_members: Vec<Addr>,    // MPC 组成员
}

// 使用 MPC 签名聚合后,通过 CW3 验证并执行
pub fn mpc_aggregated_execution(
    deps: DepsMut,
    env: Env,
    info: MessageInfo,
    proposal_id: u64,
    mpc_signatures: Vec<Binary>,
) -> StdResult<Response> {
    // 1. 验证 MPC 签名聚合
    let config = HYBRID_CONFIG.load(deps.storage)?;
    if info.sender != config.mpc_agent {
        return Err(StdError::generic_err("Unauthorized: not MPC agent"));
    }

    // 2. 验证 MPC 签名数量
    if (mpc_signatures.len() as u32) < config.mpc_threshold {
        return Err(StdError::generic_err("Insufficient MPC signatures"));
    }

    // 3. 执行 CW3 提案
    let proposal = PROPOSALS.load(deps.storage, proposal_id)?;
    if proposal.status != ProposalStatus::Passed {
        return Err(StdError::generic_err("Proposal not passed in CW3"));
    }

    proposal.status = ProposalStatus::Executed;
    PROPOSALS.save(deps.storage, proposal_id, &proposal)?;

    Ok(Response::new()
        .add_messages(proposal.msgs)
        .add_attribute("method", "hybrid_execute")
        .add_attribute("proposal_id", proposal_id.to_string())
        .add_attribute("mpc_signatures", mpc_signatures.len().to_string()))
}

8. 附录

8.1 Cargo.toml 配置

[package]
name = "cw3-fixed-multisig"
version = "1.0.0"
edition = "2021"

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

[features]
default = []
library = []

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

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

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

8.2 部署与验证命令

# 编译
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown

# 优化
wasm-opt -Os target/wasm32-unknown-unknown/release/cw3_fixed_multisig.wasm \
  -o cw3_fixed_multisig_optimized.wasm

# 获取代码 hash
sha256sum cw3_fixed_multisig_optimized.wasm

# 上传到 MSG Chain
msg-chain-devkit tx wasm store cw3_fixed_multisig_optimized.wasm \
  --from deployer --gas auto --gas-adjustment 1.3

# 验证代码
msg-chain-devkit query wasm code CODE_ID

# 实例化
msg-chain-devkit tx wasm instantiate CODE_ID '{"signers":[...]}' \
  --from admin --label "my-multisig" --admin msg1admin...

# 合约验证(通过 CosmWasm 的 verify_attestation)
msg-chain-devkit query wasm contract-state smart CONTRACT_ADDR '{"config":{}}'

8.3 Gas 成本估算

操作 Gas 消耗 (估计) 费用 (MSG)
实例化 200,000 - 400,000 0.0002 - 0.0004
创建提案 150,000 - 300,000 0.00015 - 0.0003
投票 100,000 - 200,000 0.0001 - 0.0002
执行(含消息) 200,000 - 2,000,000 0.0002 - 0.002
关闭 80,000 - 150,000 0.00008 - 0.00015
查询 10,000 - 50,000 0.00001 - 0.00005

注意:Gas 消耗取决于提案中包含的消息数量和复杂度。MSG Chain Gas 价格为 1,000,000,000 attoMSG/gas,执行包含多条消息的提案时请预留充足 Gas。

8.4 错误代码表

错误 代码 说明
Unauthorized: not a voter 1001 非授权签名者操作
Proposal has expired 1002 提案已过投票期
Already voted 1003 重复投票
Proposal not open 1004 提案非投票状态
Proposal not passed 1005 提案未通过无法执行
Already executed 1006 提案已执行
Already closed 1007 提案已关闭
Not expired yet 1008 提案未到期不能关闭
Empty proposal 1009 提案消息为空
Description too long 1010 描述超长
Too many messages 1011 超过最大消息数
Voter not found 1012 签名者不存在
Voter already exists 1013 签名者已存在
Zero weight 1014 权重为零不允许

8.5 相关资源

8.6 常见问题

Q: 签名者丢失了密钥怎么办?
A: 如果签名者丢失密钥,且多签未达到阈值,则资金永久锁定。建议:

Q: 如何更改多签签名者?
A: 使用 cw3_fixed_multisig 不能更改签名者。如需要动态成员管理,请使用 cw3_flex_multisig。

Q: 提案通过后多久可以执行?
A: 通过后可在投票期内随时执行。逾期未执行则提案过期,需重新创建。

Q: 是否可以取消已提交的投票?
A: 不可以。一旦投票即不可撤销。创建提案时请仔细审核消息内容。

Q: MSG Chain 上的最小多签阈值是多少?
A: 最小阈值为 1(即单签,不推荐),实际应至少为总签名者数的 50% + 1。

Q: 多签合约可以持有哪些资产?
A: 多签合约可以持有任何 MSG Chain 原生代币(umsg)以及 CW20 代币。对 CW20 代币的操作需要通过 Wasm 消息进行。


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