教学托管即使合约地址不变,代理也可以将调用委托给新实现。delegatecall 使用实现代码,却在代理的存储与上下文中执行;旧版 slot 装了订单 owner,新版把同一 slot 解释为 amount,就可能在不动用户地址的情况下破坏资产授权。区块链(25):把 ERC-20 接进托管后,“收到 70”仍要核算的余额约束并不能阻止管理者换上恶意或布局错误的新代码。

flowchart LR
  U[用户调用固定代理地址] --> P[Proxy: 管理升级槽/委托]
  P --> I1[实现 v1: 订单布局]
  P -->|治理批准并迁移| I2[实现 v2: 必须保持旧字段语义]
  I1 --> S[代理持久 storage]
  I2 --> S
  G[升级管理员/治理] --> P

初始化函数若公开且可再次执行,攻击者可能夺取管理员或重置订单状态。实现合约自身的初始化锁与代理首次初始化是两个不同检查;升级前后的权限、债权之和、已关闭订单不可重开以及 EIP-712 域/nonce 迁移,都应有回归。升级权限要写清控制者、延迟、执行与紧急暂停边界:管理员能否直接改规则、能否转走托管资产,决定“不可篡改”声明还能保留多少。时间锁和多签只能改变滥用门槛,不自动保证新实现正确。

升级前后同一个订单如何被解释

v1 在代理存储中把订单 A 的买家、余额、状态映射到特定槽布局。v2 若把新的管理员字段插到旧字段之前,delegatecall 会在同一组槽上用新布局解释历史字节:可能把旧余额读成权限位,或者将原有 nonce 覆盖。这不要求攻击者改代理地址,也不一定改变外部函数名。升级验收不能只比较 ABI;必须冻结编译器与布局产物,对已有订单、提现义务、授权角色、签名域与已消费 nonce 逐项复读和差量验证。初始化只能按设计允许的次数执行,未授权者不应在代理与实现合约任一入口夺权。

治理升级也是状态机:谁提交实现地址,多久之后可执行,谁能取消,紧急暂停允许冻结多长时间,用户在变更期间能否领取或退出,都改变托管承诺。如果管理员能无延迟指向可提走全部资金的实现,用户信任的就包含管理员密钥,而不是原合约代码。宣布升级计划、上传一份审核报告、完成链上升级、所有用户可兑现是四个不同事实。当前没有可运行代理或布局导出,本节只给最小破坏案例和需要验证的权限矩阵,不宣称升级已安全。

练习一:v2 在旧变量前插一个新状态变量,会有何风险?答案:旧 slot 的解释可能整体偏移,读取余额/角色失真;需冻结编译器与布局并使用差量检查。练习二:代理地址不变且 ABI 兼容,是否可跳过旧签名重放测试?答案:不能;域版本、nonce 存储与旧授权语义仍可能变化。破坏布局升级、初始化拒绝与修复回归 NOT_RUN。

可迁移原则:可升级软件的验证范围包含升级权、状态迁移和回退,而不仅是新代码的函数测试。参考:Solidity delegatecall 安全说明、OpenZeppelin Upgrades(源码 SHA/合约库版本待核);导航:区块链(25):把 ERC-20 接进托管后,“收到 70”仍要核算 · 26 · 区块链(27):响应丢失时不能凭交易哈希重做订单。