Skip to content

Upgrade Governance Hardening: bind upgradeToAndCall calldata to committee approval #16

@lurenpluto

Description

@lurenpluto

背景

当前 SourceDao 系列合约采用 UUPS 升级,实际升级入口是:

  • upgradeToAndCall(address newImplementation, bytes data)

但原有治理校验只绑定了:

  1. proxy 地址
  2. newImplementation 地址

这意味着,一旦某个实现地址被委员会提案批准,首个执行升级的人仍然可以自行选择任意 data 一起传入 upgradeToAndCall(...)

如果新实现暴露了:

  • reinitializer(...)
  • migration 函数
  • 其他初始化入口

那么治理实际上批准的是:

  • 这份实现
  • 加上任意执行 calldata

这会把迁移数据的最终决定权留给升级执行者,而不是提案本身。


问题描述

原始风险点在于:

  • 旧版 verifyContractUpgrade(...) 只校验 newImplementation
  • 不校验 upgradeToAndCall(...)data
  • 同一个 implementation 地址下,不同的 migration / init calldata 语义可能完全不同

典型风险场景:

  1. 委员会只想批准“升级到实现 A”
  2. 实现 A 同时包含一个 reinitializer(2) 或其他迁移入口
  3. 首个执行升级的人可以夹带任意 data
  4. 合约升级后执行了并未被治理明确批准的初始化逻辑

改动目标

把升级治理的授权粒度从:

  • implementation

收紧为:

  • implementation + calldata hash

也就是说,治理批准的不再只是“升级到哪个实现”,而是:

  • 升级到哪个实现
  • 是否允许附带某段特定 calldata
  • 如果允许,必须是治理批准的那一段 calldata

具体改动

1. 基类升级入口改为校验 calldata hash

SourceDaoContractUpgradeable 中重写 upgradeToAndCall(...),执行前调用:

  • committee.verifyContractUpgrade(newImplementation, keccak256(data))

只有实现地址和 data 的 hash 同时匹配时,升级才允许继续执行。

2. Committee 升级提案参数加入 calldataHash

Committee 新增支持以下重载:

  • prepareContractUpgrade(address proxy, address implementation, bytes32 calldataHash)
  • verifyContractUpgrade(address implementation, bytes32 calldataHash)

同时保留原有两参版本,语义改为:

  • 默认批准空 calldata
  • 等价于 calldataHash = keccak256("")

这样:

  • 普通无 migration 的升级流程不需要全部重写
  • 需要迁移调用的升级可以显式绑定 keccak256(data)

3. 回归测试补充

新增了针对该风险的回归测试,覆盖:

  1. 同一 implementation 地址下,未获批准的非空 calldata 会被拒绝
  2. 同一 implementation 地址下,空 calldata 仍可正常升级
  3. 显式批准 calldataHash 后,对应 migration calldata 可以正常执行
  4. legacy DaomigrateLegacyBootstrap() 升级路径必须绑定对应 migration data 的 hash

这样做的好处

本次改动的直接收益有:

  1. 升级治理的授权边界更精确
  2. 避免“治理批准实现地址,执行者自由选择 migration data”的灰区
  3. upgradeToAndCall(...) 真正成为治理审批的一部分
  4. 对无 migration 的升级路径保持较好兼容性
  5. 为后续更复杂的升级迁移流程建立明确规范

升级兼容性说明

1. 存储布局兼容

这次修改没有新增 CommitteeSourceDaoContractUpgradeable 的状态变量。

因此:

  • 不涉及新的 storage layout 风险
  • 对现有 proxy 数据布局是兼容的

2. ABI 变化

Committee 新增了两个重载接口:

  • prepareContractUpgrade(address,address,bytes32)
  • verifyContractUpgrade(address,bytes32)

原有两参接口仍然保留,因此:

  • 旧调用方式不会立即断掉
  • 但其语义现在明确等价于“只批准空 calldata 升级”

3. 运行时兼容注意事项

这里需要特别说明:

必须先升级 Committee

因为新的升级基类会调用:

  • verifyContractUpgrade(newImplementation, keccak256(data))

所以系统里的 Committee 必须先升级到支持该校验的新实现。
否则后续其他模块升级将无法按新规则运行。

推荐顺序:

  1. 先升级 Committee
  2. 再升级其他 Dao 模块

旧的未执行 upgrade proposal 需要重提

这次修改前后的 upgrade proposal 参数结构不同:

旧结构:

  • proxy + implementation + "upgradeContract"

新结构:

  • proxy + implementation + calldataHash + "upgradeContract"

因此:

  • 在升级 Committee 之前已经排队、但尚未执行的旧 upgrade proposal
  • 在升级后将不再匹配
  • 需要取消或重新发起

第一次升级 Committee 本身时,建议使用空 calldata

因为把 Committee 升到“新校验模型”的这一次升级,仍然是通过旧逻辑执行授权。

因此建议:

  • 第一次升级 Committee 到本版本时,使用 upgradeToAndCall(..., "0x")
  • 不要在这一步混入额外 migration calldata

等新 Committee 上线后,再按“实现地址 + calldata hash”规则去管理后续所有升级。


工具和调用层影响

对脚本和工具的影响主要有两点:

  1. 原有两参 prepareContractUpgrade(address,address) 仍可继续使用
    但它只适用于空 calldata 升级

  2. 如果需要显式批准 migration data,则应调用三参重载
    ethers v6 下通常需要显式签名形式,例如:

  • contract["prepareContractUpgrade(address,address,bytes32)"](...)

建议的发布与治理流程

建议把这次改动作为一次“升级治理安全收口”发布,按下面顺序执行:

  1. 清理或放弃尚未执行的旧 upgrade proposal
  2. 先将 Committee 升级到支持 calldata hash 校验的新实现
  3. 该次 Committee 升级仅使用空 calldata
  4. 之后其他模块统一按新规则升级:
    • 无 migration:两参接口即可
    • 有 migration:三参接口 + 绑定 keccak256(data)

讨论点

建议委员会重点确认以下问题:

  1. 是否接受“升级提案必须绑定 calldata hash”的更严格治理边界
  2. 是否保留两参接口作为“空 calldata 便捷入口”
  3. 是否需要在运维文档中明确写入升级顺序要求
  4. 是否需要同步更新相关工具,使其默认展示和计算 calldataHash

结论

这次修改本质上是在补齐 upgradeToAndCall(...) 的治理闭环:

  • 过去只批准 implementation
  • 现在批准 implementation + exact calldata

这样可以显著降低升级执行者在 migration / init data 上的自由度,使升级治理结果更明确、更可审计,也更符合高风险升级操作应有的审批粒度。

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions