首页
/ Union CosmWasm CW Escrow Vault:ZKGM Solver 实现与 Cosmos-EVM 可流通锁仓合约解析

Union CosmWasm CW Escrow Vault:ZKGM Solver 实现与 Cosmos-EVM 可流通锁仓合约解析

2026-09-06 11:22:33作者:郜逊炳

本文基于 Union 协议仓库中的 cw-escrow-vault 合约文档 展开,系统讲解该 CosmWasm 合约如何在 UCS03 ZKGM 协议中充当 solver(做市商),完成 Cosmos 与 EVM 链之间的可流通(fungible)资产跨链转移。读完本文后,你将理解其 fungible lane 配置模型、DoSolve 填充订单的完整执行链路、意图白名单机制,以及权限、升级和测试层面的源码实现细节。

一、合约定位:ZKGM 协议中的 Cosmos 侧锁仓方

CW Escrow Vault 是一个 CosmWasm 智能合约,在 UCS03 ZKGM 协议中充当 solver 角色,支持 Cosmos 链与 EVM 链之间原生代币(bank 资产)与 CW20 代币的可流通跨链转移。它负责托管(escrow)和释放代币,是与 EVM 侧 UnionversalToken.sol 合约相对应的 Cosmos 侧对手方。

ZKGM 协议的跨链转账采用"开放填充(open filling)"机制:

  1. 做市商 / solver 可以在目标链上提供流动性来填充订单;
  2. 回执(acknowledgement)中携带"谁填充了订单"(即 beneficiary)的信息;
  3. 源链将 base 代币作为补偿发送给填充方。

CW Escrow Vault 实现了 ZKGM 的 ISolver 接口,以自动化做市商的身份参与该体系,从而在两条链之间建立起一条"可流通车道"(fungible lane)。

三方组件关系

从文档与仓库结构看,该体系由三个组件构成:

  • CW Escrow Vault(Cosmos):即本合约,用托管的代币填充订单;
  • UnionversalToken.sol(EVM):EVM 侧对手方,负责铸造/销毁合成代币,同样充当 solver(见 evm/contracts/UnionversalToken.sol);
  • UCS03 ZKGM 协议:带有开放填充机制的跨链消息协议。

两侧 vault 均被配置为 solver,counterparty_beneficiary 的取值决定了方向语义:

  • Cosmos → EVM 方向:Escrow Vault 将 UnionversalToken.sol 的地址存为 counterparty_beneficiary
  • EVM → Cosmos 方向:Escrow Vault 返回零地址,触发对端代币的销毁;
  • 填充订单时,vault 在 acknowledgement 中返回相应 beneficiary;
  • 源链据此要么把 base 代币发给 beneficiary,要么(当 beneficiary 为 0 时)直接销毁。

二、双向转账流程

正向转账(Cosmos → EVM)

  1. 用户发起转账:通过 ZKGM 以 TokenOrderV2 订单发送原生/CW20 代币;
  2. 包发送到 EVM:ZKGM 通过 IBC 将 packet 发送到目标链;
  3. UnionversalToken.sol 填充订单:向接收者铸造合成代币,并在 acknowledgement 中返回 CW Escrow Vault 地址作为 beneficiary;
  4. base 代币进入 vault:Cosmos 侧 ZKGM 将 base 代币发送给 Escrow Vault;
  5. 代币进入托管:vault 持有这些代币,供后续反向转账释放。

反向转账(EVM → Cosmos)

  1. 用户发起转账:在 EVM 侧通过 ZKGM 以 TokenOrderV2 订单发送合成代币;
  2. 包发送到 Cosmos:ZKGM 通过 IBC 将 packet 发送到目标链;
  3. Escrow Vault 填充订单:释放托管代币给接收者,并在 acknowledgement 中返回零地址(0x0)作为 beneficiary;
  4. base 代币被销毁:由于 beneficiary 为 0x0,EVM 侧 ZKGM 销毁对应合成代币;
  5. 供应量守恒:EVM 侧销毁与 Cosmos 侧从托管释放相结合,维持了整体代币供应量的 1:1 锚定。

三、Fungible Lane 配置:存储模型与消息定义

SetFungibleCounterparty 消息

合约通过 RestrictedExecuteMsg::SetFungibleCounterparty 写入一条 fungible lane,消息定义位于 src/msg.rs

SetFungibleCounterparty {
    path: U256,                    // 路由路径标识符
    channel_id: ChannelId,         // IBC 通道
    base_token: Bytes,            // 源链上的代币标识
    counterparty_beneficiary: Bytes, // EVM 上 UnionversalToken.sol 的地址
    escrowed_denom: String,       // 本地用于填充订单的托管代币
}

各参数的作用:

  • path + channel_id + base_token 三元组构成 lane 的唯一键
  • counterparty_beneficiary 是 acknowledgement 中返回的 beneficiary 来源(见下文 Solver 事件);
  • escrowed_denom 指定该 lane 用哪个本地 denom 来填充订单,必须是合法 UTF-8 denom。

存储布局

src/state.rs 可以确认合约使用 depolama 存储框架管理三块状态:

存储项 键类型 值类型 说明
FungibleCounterparty (U256, ChannelId, Bytes) FungibleLane lane 配置表,前缀 fungible_counterpartystate.rs#L30-L42
IntentWhitelist H256 bool 意图包哈希白名单,前缀 intent_whiteliststate.rs#L9-L21
Zkgm () Addr 唯一有权调用 DoSolve 的 ZKGM 合约地址(state.rs#L44-L53

其中 lane 值 FungibleLane 包含三个字段(state.rs#L23-L28):

pub struct FungibleLane {
    pub counterparty_beneficiary: Bytes,
    pub escrowed_denom: String,
    pub is_cw20: bool,   // 托管代币是否为 CW20 合约地址
}

值得注意的实现细节是:is_cw20 并非由配置者手工指定,而是在处理 SetFungibleCounterparty 时由合约自动探测——源码对 escrowed_denom 执行 query_wasm_contract_info,若查询成功(即该地址是 wasm 合约)则判定为 CW20(contract.rs#L214-L226)。这一判定直接决定后续转账走 BankMsg::Send 还是 Cw20ExecuteMsg::Transfer

四、Solver 接口:消息与查询定义

合约的 ExecuteMsg 是一个三段式枚举(msg.rs#L26-L33):

pub enum ExecuteMsg {
    #[serde(untagged)]
    Solvable(Solvable),                          // ZKGM solver 入口:DoSolve
    #[serde(untagged)]
    AccessManaged(access_managed::ExecuteMsg),   // 访问控制管理
    #[serde(untagged)]
    Restricted(Restricted<RestrictedExecuteMsg>),// 受权限保护的配置入口
}
  • Solvable:来自共享 crate lib/ucs03-solvable/src/lib.rs,定义了协议统一的 solver 接口,确保所有 Cosmos 侧 solver 合约的消息格式一致;
  • Restricted:包裹了 WhitelistIntentsSetFungibleCounterpartyUpgradable 三个受 access-managed 权限框架保护的入口(msg.rs#L39-L52)。

DoSolve 载荷结构

Solvable::DoSolve 由 ZKGM 合约发起,其字段定义在 lib/ucs03-solvable/src/lib.rs#L9-L19

DoSolve {
    packet: Packet,       // IBC packet
    order: CwTokenOrderV2, // 待填充的订单
    path: U256,          // 路由路径
    caller: Addr,        // 原始调用者
    relayer: Addr,       // 中继者(费用接收方)
    relayer_msg: Bytes,  // 中继者附带的消息
    intent: bool,        // 是否为意图单(需白名单)
}

订单本体 CwTokenOrderV2lib.rs#L24-L33)的关键字段:

字段 类型 含义
sender Bytes 源链订单发起者
receiver Bytes 目标链接收者(Cosmos bech32 地址的原始字节)
base_token Bytes 源链基础代币标识
base_amount U256 源链基础代币数量
quote_token Bytes 本链用于支付的代币(须与 lane 的 escrowed_denom 一致)
quote_amount U256 实际支付给接收者的数量
kind u8 订单类型
metadata Bytes 扩展元数据

查询接口方面,QueryMsgmsg.rs#L57-L68)提供:

  • GetFungibleCounterparty { path, channel_id, base_token }:按三元组查询单条 lane;
  • GetAllFungibleCounterparties {}:升序遍历全部 lane,返回 FungibleLaneConfig 向量(含 is_cw20 字段,msg.rs#L70-L78);
  • Solvable(SolverQuery):即 README 中提到的 solver 能力探测查询。

SolverQuerylib.rs#L40-L45)包含两个变体,合约在 contract.rs#L275-L276 中的实际实现为:

  • IsSolver:返回 unit(空 JSON),表示"我是 solver";
  • AllowMarketMakers:返回 true,表示允许其他做市商代为履约。

五、DoSolve 执行链路逐行解析

DoSolve 的完整处理逻辑位于 contract.rs#L87-L186,执行顺序与校验要点如下。

1. ZKGM 单点入口校验

ensure_zkgm(deps.as_ref(), &info)?;  // contract.rs#L71-L77

合约从 Zkgm 存储中读取唯一授权的 ZKGM 地址,若 info.sender 与之不符则返回 ContractError::OnlyZkgm。这保证了任何外部地址都无法直接触发填充逻辑。

2. 意图单白名单校验(一次性消费)

intent == true 时,合约用 commit_packets(slice::from_ref(&packet)) 计算 packet 的承诺哈希,然后查 IntentWhitelist:未命中则返回 IntentMustBeWhitelisted;命中后立即 删除该白名单条目contract.rs#L98-L111)。也就是说每个被白名单化的意图包只能被消费一次,天然防重放。

3. Fungible Lane 校验

合约以 (path, packet.destination_channel_id, order.base_token) 三元组查询 FungibleCounterparty,查不到则返回 LaneIsNotFungible { channel_id }

4. 报价代币一致性校验

order.quote_token 必须是合法 UTF-8(否则 InvalidQuoteToken),且必须严格等于 lane 配置的 escrowed_denom,否则返回 InvalidFill { quote_token, escrowed_denom }。这防止 solver 用错误的代币去填单。

5. 费用拆分与两笔转账

let fee = order.base_amount.checked_sub(order.quote_amount)
    .ok_or_else(|| ContractError::BaseAmountMustCoverQuoteAmount)?;
push_transfer(relayer.into(), fee.try_into().expect("impossible"))?;
// ... 校验 receiver 为合法 bech32 后
push_transfer(receiver.into(), order.quote_amount.try_into().expect("impossible"))?;
  • 费用 = base_amount - quote_amount,支付给 relayer。若 base_amount < quote_amount(checked_sub 溢出),直接失败,保证费用非负;
  • 接收者order.receiver 原始字节先转为 UTF-8 字符串,再经 api.addr_validate 校验为合法 bech32 地址(否则 InvalidReceiver),支付 quote_amount
  • 两笔转账均通过内部闭包 push_transferis_cw20 标志分派:CW20 lane 发出 wasm_execute(Cw20ExecuteMsg::Transfer),原生币 lane 发出 BankMsg::Send;数量为 0 时不产生消息(contract.rs#L134-L163)。

6. 通过 Solver 事件返回 beneficiary

Ok(Response::new().add_messages(messages).add_event(Solver {
    market_maker: fungible_lane.counterparty_beneficiary,
}))

合约把 lane 中配置的 counterparty_beneficiary 放入 Solver 事件(contract.rs#L183-L185)。ZKGM 从该事件读取 beneficiary 写入 acknowledgement,从而完成文档中"vault 在回执中返回 beneficiary"的闭环——正向方向它是 UnionversalToken.sol 地址,反向方向它是零地址以触发销毁。

失败模式一览

所有失败分支定义在 src/error.rs 中,与文档"Security Features"一节逐条对应:

错误变体 触发条件
OnlyZkgm 调用 DoSolve 的 sender 不是配置的 ZKGM 合约
IntentMustBeWhitelisted intent 单未在白名单中
LaneIsNotFungible { channel_id } (path, channel, base_token) 未配置 fungible lane
InvalidQuoteToken quote_token 非合法 UTF-8
InvalidFill { quote_token, escrowed_denom } 报价代币与 lane 托管 denom 不一致
BaseAmountMustCoverQuoteAmount base 数量不足以覆盖 quote 数量(费用为负)
InvalidReceiver 接收者不是合法 bech32 地址

六、意图白名单机制

WhitelistIntents 是唯一的意图管理入口(contract.rs#L199-L206):

WhitelistIntents {
    hashes_whitelist: Vec<(H256, bool)>, // (包承诺哈希, 是否批准)
}

实现上它就是对 IntentWhitelist 存储的批量写入:true 表示批准、false 表示撤销(或覆盖已有条目)。配合 DoSolve 中的"命中即删除"逻辑,形成"预批准一次、消费一次"的语义。典型用途是运营方为已知的大额跨链转账提前锁定填充资格,防止恶意抢跑者抢占 intent 单。

七、权限模型与访问控制

合约的权限体系由两层构成:

  1. access-managed 权限框架SetFungibleCounterpartyWhitelistIntents 被包在 Restricted<RestrictedExecuteMsg> 中,执行前经过 ensure_can_call::<Authority> 鉴权(contract.rs#L190-L197);未授权调用会得到 AccessManagedUnauthorized 错误(对应测试 set_fungible_counterparty_fails_when_not_admincontract.rs#L800-L840)。
  2. Zkgm 单点授权:仅 DoSolveensure_zkgm 硬校验,其他配置入口与 ZKGM 地址无关。

对应 README "Security Features" 的四条声明均可在源码中找到落点:

  • Admin-only 配置Restricted 入口经 access-managed 鉴权;
  • ZKGM-only 执行ensure_zkgmOnlyZkgm 错误有专门测试 solve_fails_when_caller_is_not_zkgmcontract.rs#L745-L797);
  • Lane 校验:非 fungible lane 拒填(LaneIsNotFungible);
  • Intent 保护:未白名单化的意图单拒绝(IntentMustBeWhitelisted)。

八、部署模式与状态升级

与 README "Integration" 中"部署 vault"的描述不同,源码显示该合约采用了 frissitheto 升级框架instantiate 入口直接 panic!contract.rs#L41-L46):

pub fn instantiate(_: DepsMut, _: Env, _: MessageInfo, _: ()) -> StdResult<Response> {
    panic!("this contract cannot be instantiated directly, but must be migrated from an existing instantiated contract.");
}

从源码结构看,其初始化必须通过 migrate 入口以 UpgradeMsg::Init(InstantiateMsg) 完成(contract.rs#L48-L69):

pub struct InstantiateMsg {
    pub zkgm: Addr,
    pub access_managed_init_msg: access_managed::InitMsg,
}

初始化过程做两件事:调用 access_managed::init 写入初始权限(initial_authority),并把 zkgm 地址写入 Zkgm 存储。合约维护了状态版本号(contract.rs#L25-L39):

  • version::INIT(v1):访问管理内建于合约内部;
  • version::MANAGED(v2,当前 LATEST):访问管理外置到 access-managed crate,并移除了内部权限存储;
  • INIT 迁移到 MANAGED 会被显式拒绝(unsupported version: INIT),即 v1 部署不能原地升级。

reply 入口(contract.rs#L281-L288)则用于处理 access-managed 的"计划操作(scheduled op)"回执——当权限框架要求把敏感操作延迟执行时,Restricted 分支会返回子消息(EnsureCanCallResult::Scheduled),其结果在此回调中消费。

九、测试用例对行为的验证

合约内置了较完整的单测模块(contract.rs#L290-L878),每一篇关键断言都对应本文前述逻辑:

测试 验证的行为
solve_successful 未配置 lane 时返回 LaneIsNotFungible;配置后 150/150 填单产生 BankMsg::Sendmuno × 150),事件为 Solver { market_maker: [0;32] };0/0 订单不产生任何转账消息
solve_successful_with_cw20_fungible_lane escrowed_denom 是 wasm 合约(mock 返回 ContractInfo 成功)时,转账改走 Cw20ExecuteMsg::Transfer
solve_with_excess_fee 150/100 订单:50 费用先发给 relayer,100 再发给接收者,消息顺序固定
solve_with_intent commit_packets 计算承诺并白名单后,intent 单成功填充
solve_fails_when_intent_not_whitelisted 未白名单的 intent 单被拒
solve_fails_when_quote_token_is_wrong lane 的 escrowed_denomquote_token 不匹配时返回 InvalidFill
solve_fails_when_base_amount_doesnt_cover_quote_amount base < quote 时返回 BaseAmountMustCoverQuoteAmount
solve_fails_when_caller_is_not_zkgm 非 ZKGM sender 被拒(OnlyZkgm
set_fungible_counterparty_fails_when_not_admin / whitelist_admin_fails_when_not_admin 非授权调用配置入口返回 AccessManagedUnauthorized

测试中 mock_solve 构造的 CwTokenOrderV2 使用 quote_token: b"muno"base_token: b"base_token"path: 0 等取值(contract.rs#L339-L358),可作为理解各字段取值的参考样例。

十、集成步骤与收益总结

按文档给出的集成流程,并补充源码层面的注意事项:

  1. 部署 vault:以 zkgmaccess_managed_init_msg(含初始权限方)为参数初始化(注意实际需经 migrate 入口走 UpgradeMsg::Init);
  2. 配置 fungible lanes:为每个 (代币, 通道) 对调用 SetFungibleCounterpartyescrowed_denom 会被自动探测为 CW20 或原生币;
  3. 设置 counterparty beneficiary:填对端链对应 vault 的地址,反向方向填零地址;
  4. (可选)为 vault 注资:注入用于填单的托管代币;
  5. (可选)白名单化 intents:通过 WhitelistIntents 为预批准的转账提前放行。

配置完成后,vault 自动作为 ZKGM 协议中的 solver 参与填单,在两条链之间维持可流通性。其设计收益可归纳为:

  1. 资本效率:无需独立流动性池,直接使用托管代币;
  2. 即时填充:对已配置 lane 充当始终在线的做市商;
  3. 费用激励:relayer 通过 base_amount - quote_amount 的差价获得填单费用;
  4. 真实可流通性:维持原生代币与合成代币的 1:1 锚定;
  5. 无信任运行:合约化自动化消除了对手方风险。

十一、相关文件索引

登录后查看全文
热门项目推荐
相关项目推荐