区块链(20):教学托管合约先写清谁能推动状态
合成订单的付款进入教学托管,不应立刻把款转给卖家。区块链(19):revert 以后为什么仍要核算 Gas 与 nonce已经区分提交与执行;合约还需为订单建立明确的状态迁移。买方创建并锁定指定整数单位的款,卖方只能提交交付声明,买方或经约定的裁决者确认后进入待领取,争议或超时按预先冻结的规则退款。交付声明不是现实交付事实,确认者与资金控制权也不能暗中换成部署管理员。
stateDiagram-v2
[*] --> Funded: 买方创建、校验金额
Funded --> Delivered: 卖方声明交付
Delivered --> Claimable: 被授权方确认
Funded --> Refundable: 满足取消条件
Delivered --> Refundable: 到期/裁决规则
Claimable --> Paid: 卖方领取
Refundable --> Refunded: 买方领取
安全不变量包括同一订单只能进入一个终态、账本记载的待领取义务不超过可支配资产、未经授权者不能确认或转移款。活性则要求领取者发起交易并支付费用;状态机不会在区块高度达到期限时自行广播。账本保存订单 ID、买卖方、金额、截止条件和可领取余额,订单 ID 的来源要防重;禁止把 tx.origin 当授权身份,也不要在外部调用之前标记“已支付”。
托管金与两种互斥债权
合成订单 A 锁入 70 单位原生资产。进入 Funded 时,合约有 70 的待裁决义务,却不能同时给卖家和买家各建立 70 的可提现额度。卖家声明交付只改变业务状态,不能转移币;买方有效确认后,买家退款路径应关闭,卖家出现 70 的待领取债权;若合法的超时/争议策略选择退款,则卖家领取路径关闭、买方得到同额债权。终态 Paid 和 Refunded 不可同时到达。提现应先消费相应债权、再调用收款方并处理失败,这也是 23 篇的重入先修。把超时写成“合约到了高度自动付款”会漏掉谁发起、谁付 gas、外部调用失败之后钱放在哪里。
测试矩阵不能只覆盖 Funded→Delivered→Claimable→Paid。例如卖家尚未提交交付时就试图提款,陌生用户发出确认,买方在确认和超时退款之间竞争排序,或对同一订单 ID 再存一份 70,都应该落在明确的拒绝/先到先得状态。examples/blockchain/models/escrow_state.py 穷举了一个订单、金额 1/2 和重复领取短反例,但它没有买卖角色、回调、链上余额或 Solidity 编译结果;不能作为本篇的 Forge 验收。实际合约还缺代码、部署配置和负/正路径原始回执,因此这里是精确状态规格而非合约交付。
正常路径是资金进入→声明→确认→领取;拒绝路径是金额为 0、重复订单 ID、陌生角色确认、交付前卖家领取与领取两次。练习一:订单 Funded 时卖家调用 withdraw,应发生什么?答案:没有可领取债权,状态不变且调用拒绝。练习二:卖家说已发货但买家拒绝确认,合约能证明谁诚实吗?答案:不能;需要链外凭证或裁决规则,签名和事件仅证明消息曾提交。
Forge、Solidity 版本、优化参数、Anvil 链 ID 都未冻结,以上为待实现状态机设计而非部署结果;正常、拒绝和提现回归 NOT_RUN。可迁移原则:先写角色、资产义务和失败出口,再写函数。参考:Solidity 安全说明、Foundry Book(原文访问与源码 SHA 待核);导航:区块链(19):revert 以后为什么仍要核算 Gas 与 nonce · 20 · 区块链(21):ABI、数据位置与事件各承诺什么。





