-
Notifications
You must be signed in to change notification settings - Fork 2
Upgrade Governance Hardening: bind upgradeToAndCall calldata to committee approval #16
Description
背景
当前 SourceDao 系列合约采用 UUPS 升级,实际升级入口是:
upgradeToAndCall(address newImplementation, bytes data)
但原有治理校验只绑定了:
proxy地址newImplementation地址
这意味着,一旦某个实现地址被委员会提案批准,首个执行升级的人仍然可以自行选择任意 data 一起传入 upgradeToAndCall(...)。
如果新实现暴露了:
reinitializer(...)- migration 函数
- 其他初始化入口
那么治理实际上批准的是:
- 这份实现
- 加上任意执行 calldata
这会把迁移数据的最终决定权留给升级执行者,而不是提案本身。
问题描述
原始风险点在于:
- 旧版
verifyContractUpgrade(...)只校验newImplementation - 不校验
upgradeToAndCall(...)的data - 同一个 implementation 地址下,不同的 migration / init calldata 语义可能完全不同
典型风险场景:
- 委员会只想批准“升级到实现 A”
- 实现 A 同时包含一个
reinitializer(2)或其他迁移入口 - 首个执行升级的人可以夹带任意
data - 合约升级后执行了并未被治理明确批准的初始化逻辑
改动目标
把升级治理的授权粒度从:
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. 回归测试补充
新增了针对该风险的回归测试,覆盖:
- 同一 implementation 地址下,未获批准的非空 calldata 会被拒绝
- 同一 implementation 地址下,空 calldata 仍可正常升级
- 显式批准
calldataHash后,对应 migration calldata 可以正常执行 - legacy
Dao的migrateLegacyBootstrap()升级路径必须绑定对应 migration data 的 hash
这样做的好处
本次改动的直接收益有:
- 升级治理的授权边界更精确
- 避免“治理批准实现地址,执行者自由选择 migration data”的灰区
- 让
upgradeToAndCall(...)真正成为治理审批的一部分 - 对无 migration 的升级路径保持较好兼容性
- 为后续更复杂的升级迁移流程建立明确规范
升级兼容性说明
1. 存储布局兼容
这次修改没有新增 Committee 或 SourceDaoContractUpgradeable 的状态变量。
因此:
- 不涉及新的 storage layout 风险
- 对现有 proxy 数据布局是兼容的
2. ABI 变化
Committee 新增了两个重载接口:
prepareContractUpgrade(address,address,bytes32)verifyContractUpgrade(address,bytes32)
原有两参接口仍然保留,因此:
- 旧调用方式不会立即断掉
- 但其语义现在明确等价于“只批准空 calldata 升级”
3. 运行时兼容注意事项
这里需要特别说明:
必须先升级 Committee
因为新的升级基类会调用:
verifyContractUpgrade(newImplementation, keccak256(data))
所以系统里的 Committee 必须先升级到支持该校验的新实现。
否则后续其他模块升级将无法按新规则运行。
推荐顺序:
- 先升级
Committee - 再升级其他
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”规则去管理后续所有升级。
工具和调用层影响
对脚本和工具的影响主要有两点:
-
原有两参
prepareContractUpgrade(address,address)仍可继续使用
但它只适用于空 calldata 升级 -
如果需要显式批准 migration data,则应调用三参重载
在ethers v6下通常需要显式签名形式,例如:
contract["prepareContractUpgrade(address,address,bytes32)"](...)
建议的发布与治理流程
建议把这次改动作为一次“升级治理安全收口”发布,按下面顺序执行:
- 清理或放弃尚未执行的旧 upgrade proposal
- 先将
Committee升级到支持 calldata hash 校验的新实现 - 该次
Committee升级仅使用空 calldata - 之后其他模块统一按新规则升级:
- 无 migration:两参接口即可
- 有 migration:三参接口 + 绑定
keccak256(data)
讨论点
建议委员会重点确认以下问题:
- 是否接受“升级提案必须绑定 calldata hash”的更严格治理边界
- 是否保留两参接口作为“空 calldata 便捷入口”
- 是否需要在运维文档中明确写入升级顺序要求
- 是否需要同步更新相关工具,使其默认展示和计算
calldataHash
结论
这次修改本质上是在补齐 upgradeToAndCall(...) 的治理闭环:
- 过去只批准 implementation
- 现在批准 implementation + exact calldata
这样可以显著降低升级执行者在 migration / init data 上的自由度,使升级治理结果更明确、更可审计,也更符合高风险升级操作应有的审批粒度。
Metadata
Metadata
Assignees
Labels
Type
Projects
Status