背景
当前 SourceDao 系列合约采用 UUPS 升级,实际升级入口是:
upgradeToAndCall(address newImplementation, bytes data)
但原有治理校验只绑定了:
proxy 地址
newImplementation 地址
这意味着,一旦某个实现地址被委员会提案批准,首个执行升级的人仍然可以自行选择任意 data 一起传入 upgradeToAndCall(...)。
如果新实现暴露了:
reinitializer(...)
- migration 函数
- 其他初始化入口
那么治理实际上批准的是:
这会把迁移数据的最终决定权留给升级执行者,而不是提案本身。
问题描述
原始风险点在于:
- 旧版
verifyContractUpgrade(...) 只校验 newImplementation
- 不校验
upgradeToAndCall(...) 的 data
- 同一个 implementation 地址下,不同的 migration / init calldata 语义可能完全不同
典型风险场景:
- 委员会只想批准“升级到实现 A”
- 实现 A 同时包含一个
reinitializer(2) 或其他迁移入口
- 首个执行升级的人可以夹带任意
data
- 合约升级后执行了并未被治理明确批准的初始化逻辑
改动目标
把升级治理的授权粒度从:
收紧为:
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 上的自由度,使升级治理结果更明确、更可审计,也更符合高风险升级操作应有的审批粒度。
背景
当前
SourceDao系列合约采用 UUPS 升级,实际升级入口是:upgradeToAndCall(address newImplementation, bytes data)但原有治理校验只绑定了:
proxy地址newImplementation地址这意味着,一旦某个实现地址被委员会提案批准,首个执行升级的人仍然可以自行选择任意
data一起传入upgradeToAndCall(...)。如果新实现暴露了:
reinitializer(...)那么治理实际上批准的是:
这会把迁移数据的最终决定权留给升级执行者,而不是提案本身。
问题描述
原始风险点在于:
verifyContractUpgrade(...)只校验newImplementationupgradeToAndCall(...)的data典型风险场景:
reinitializer(2)或其他迁移入口data改动目标
把升级治理的授权粒度从:
implementation收紧为:
implementation + calldata hash也就是说,治理批准的不再只是“升级到哪个实现”,而是:
具体改动
1. 基类升级入口改为校验 calldata hash
在
SourceDaoContractUpgradeable中重写upgradeToAndCall(...),执行前调用:committee.verifyContractUpgrade(newImplementation, keccak256(data))只有实现地址和
data的 hash 同时匹配时,升级才允许继续执行。2. Committee 升级提案参数加入
calldataHashCommittee新增支持以下重载:prepareContractUpgrade(address proxy, address implementation, bytes32 calldataHash)verifyContractUpgrade(address implementation, bytes32 calldataHash)同时保留原有两参版本,语义改为:
calldataHash = keccak256("")这样:
keccak256(data)3. 回归测试补充
新增了针对该风险的回归测试,覆盖:
calldataHash后,对应 migration calldata 可以正常执行Dao的migrateLegacyBootstrap()升级路径必须绑定对应 migration data 的 hash这样做的好处
本次改动的直接收益有:
upgradeToAndCall(...)真正成为治理审批的一部分升级兼容性说明
1. 存储布局兼容
这次修改没有新增
Committee或SourceDaoContractUpgradeable的状态变量。因此:
2. ABI 变化
Committee新增了两个重载接口:prepareContractUpgrade(address,address,bytes32)verifyContractUpgrade(address,bytes32)原有两参接口仍然保留,因此:
3. 运行时兼容注意事项
这里需要特别说明:
必须先升级 Committee
因为新的升级基类会调用:
verifyContractUpgrade(newImplementation, keccak256(data))所以系统里的
Committee必须先升级到支持该校验的新实现。否则后续其他模块升级将无法按新规则运行。
推荐顺序:
CommitteeDao模块旧的未执行 upgrade proposal 需要重提
这次修改前后的 upgrade proposal 参数结构不同:
旧结构:
proxy + implementation + "upgradeContract"新结构:
proxy + implementation + calldataHash + "upgradeContract"因此:
Committee之前已经排队、但尚未执行的旧 upgrade proposal第一次升级 Committee 本身时,建议使用空 calldata
因为把
Committee升到“新校验模型”的这一次升级,仍然是通过旧逻辑执行授权。因此建议:
Committee到本版本时,使用upgradeToAndCall(..., "0x")等新
Committee上线后,再按“实现地址 + calldata hash”规则去管理后续所有升级。工具和调用层影响
对脚本和工具的影响主要有两点:
原有两参
prepareContractUpgrade(address,address)仍可继续使用但它只适用于空 calldata 升级
如果需要显式批准 migration data,则应调用三参重载
在
ethers v6下通常需要显式签名形式,例如:contract["prepareContractUpgrade(address,address,bytes32)"](...)建议的发布与治理流程
建议把这次改动作为一次“升级治理安全收口”发布,按下面顺序执行:
Committee升级到支持 calldata hash 校验的新实现Committee升级仅使用空 calldatakeccak256(data)讨论点
建议委员会重点确认以下问题:
calldataHash结论
这次修改本质上是在补齐
upgradeToAndCall(...)的治理闭环:这样可以显著降低升级执行者在 migration / init data 上的自由度,使升级治理结果更明确、更可审计,也更符合高风险升级操作应有的审批粒度。