Union CosmWasm CW Escrow Vault:ZKGM Solver 实现与 Cosmos-EVM 可流通锁仓合约解析
本文基于 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)"机制:
- 做市商 / solver 可以在目标链上提供流动性来填充订单;
- 回执(acknowledgement)中携带"谁填充了订单"(即 beneficiary)的信息;
- 源链将 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)
- 用户发起转账:通过 ZKGM 以
TokenOrderV2订单发送原生/CW20 代币; - 包发送到 EVM:ZKGM 通过 IBC 将 packet 发送到目标链;
- UnionversalToken.sol 填充订单:向接收者铸造合成代币,并在 acknowledgement 中返回 CW Escrow Vault 地址作为 beneficiary;
- base 代币进入 vault:Cosmos 侧 ZKGM 将 base 代币发送给 Escrow Vault;
- 代币进入托管:vault 持有这些代币,供后续反向转账释放。
反向转账(EVM → Cosmos)
- 用户发起转账:在 EVM 侧通过 ZKGM 以
TokenOrderV2订单发送合成代币; - 包发送到 Cosmos:ZKGM 通过 IBC 将 packet 发送到目标链;
- Escrow Vault 填充订单:释放托管代币给接收者,并在 acknowledgement 中返回零地址(0x0)作为 beneficiary;
- base 代币被销毁:由于 beneficiary 为 0x0,EVM 侧 ZKGM 销毁对应合成代币;
- 供应量守恒: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_counterparty(state.rs#L30-L42) |
IntentWhitelist |
H256 |
bool |
意图包哈希白名单,前缀 intent_whitelist(state.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:包裹了WhitelistIntents、SetFungibleCounterparty与Upgradable三个受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, // 是否为意图单(需白名单)
}
订单本体 CwTokenOrderV2(lib.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 |
扩展元数据 |
查询接口方面,QueryMsg(msg.rs#L57-L68)提供:
GetFungibleCounterparty { path, channel_id, base_token }:按三元组查询单条 lane;GetAllFungibleCounterparties {}:升序遍历全部 lane,返回FungibleLaneConfig向量(含is_cw20字段,msg.rs#L70-L78);Solvable(SolverQuery):即 README 中提到的 solver 能力探测查询。
SolverQuery(lib.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_transfer按is_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 单。
七、权限模型与访问控制
合约的权限体系由两层构成:
access-managed权限框架:SetFungibleCounterparty与WhitelistIntents被包在Restricted<RestrictedExecuteMsg>中,执行前经过ensure_can_call::<Authority>鉴权(contract.rs#L190-L197);未授权调用会得到AccessManagedUnauthorized错误(对应测试set_fungible_counterparty_fails_when_not_admin,contract.rs#L800-L840)。Zkgm单点授权:仅DoSolve走ensure_zkgm硬校验,其他配置入口与 ZKGM 地址无关。
对应 README "Security Features" 的四条声明均可在源码中找到落点:
- Admin-only 配置:
Restricted入口经 access-managed 鉴权; - ZKGM-only 执行:
ensure_zkgm(OnlyZkgm错误有专门测试solve_fails_when_caller_is_not_zkgm,contract.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-managedcrate,并移除了内部权限存储;- 从
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::Send(muno × 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_denom 与 quote_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),可作为理解各字段取值的参考样例。
十、集成步骤与收益总结
按文档给出的集成流程,并补充源码层面的注意事项:
- 部署 vault:以
zkgm与access_managed_init_msg(含初始权限方)为参数初始化(注意实际需经migrate入口走UpgradeMsg::Init); - 配置 fungible lanes:为每个 (代币, 通道) 对调用
SetFungibleCounterparty,escrowed_denom会被自动探测为 CW20 或原生币; - 设置 counterparty beneficiary:填对端链对应 vault 的地址,反向方向填零地址;
- (可选)为 vault 注资:注入用于填单的托管代币;
- (可选)白名单化 intents:通过
WhitelistIntents为预批准的转账提前放行。
配置完成后,vault 自动作为 ZKGM 协议中的 solver 参与填单,在两条链之间维持可流通性。其设计收益可归纳为:
- 资本效率:无需独立流动性池,直接使用托管代币;
- 即时填充:对已配置 lane 充当始终在线的做市商;
- 费用激励:relayer 通过
base_amount - quote_amount的差价获得填单费用; - 真实可流通性:维持原生代币与合成代币的 1:1 锚定;
- 无信任运行:合约化自动化消除了对手方风险。
十一、相关文件索引
- 合约文档:cosmwasm/cw-escrow-vault/README.md
- 合约主逻辑与测试:cosmwasm/cw-escrow-vault/src/contract.rs
- 消息定义:cosmwasm/cw-escrow-vault/src/msg.rs
- 存储定义:cosmwasm/cw-escrow-vault/src/state.rs
- 错误定义:cosmwasm/cw-escrow-vault/src/error.rs
- 依赖清单:cosmwasm/cw-escrow-vault/Cargo.toml(关键依赖:
access-managed、cw20、depolama、frissitheto、ibc-union-spec、ucs03-solvable、ucs03-zkgm、upgradable) - Solver 接口共享定义:lib/ucs03-solvable/src/lib.rs
- EVM 侧对手方合约:evm/contracts/UnionversalToken.sol
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00