-
Notifications
You must be signed in to change notification settings - Fork 2
SourceDao 初始化收口与老版本升级兼容说明:bootstrap admin / finalize / migrateLegacyBootstrap #15
Description
背景
本次改动聚焦 SourceDao 的初始化部署阶段收口,以及老版本 SourceDao proxy 升级到新实现时的兼容处理。
在旧实现中,SourceDao 的模块地址初始化存在两个核心问题:
- 任意地址都可以抢先写入未初始化的模块 slot
- 模块 slot 只要求非零地址,不要求目标地址是合约,EOA 也能被写入正式模块地址
这两个问题都发生在 bootstrap 阶段,因此本次不是用长期治理权限去处理,而是引入一套短生命周期的初始化控制模型。
本次改动目标
本次改动的目标有五个:
- 为模块地址初始化建立明确的 bootstrap 权限边界
- 阻止 EOA 被写入正式模块 slot
- 允许部署阶段修正配置错误
- 在 bootstrap 完成后显式冻结配置入口
- 兼容老版本
SourceDaoproxy 升级到新实现
具体改动
1. 引入 bootstrap admin
SourceDao.initialize() 现在会记录:
bootstrapAdmin = msg.senderbootstrapFinalized = false
后续所有 setXAddress(...) 都要求:
- 调用者必须是
bootstrapAdmin - 当前必须尚未 finalize
也就是说,模块地址不再能被任意 caller 抢先写入。
2. 模块地址校验收紧
模块地址现在不再只检查非零,而是要求:
- 地址不为
0 code.length > 0
这样正式模块 slot 只接受合约地址,不再接受 EOA。
3. 引入 finalizeInitialization()
新增 finalizeInitialization()。
只有在全部核心模块地址都已配置完成后,bootstrapAdmin 才能调用该函数。
一旦 finalize 成功:
bootstrapFinalized = true- 所有
setXAddress(...)永久关闭 SourceDaobootstrap 阶段结束
4. 调整原来的 “set once” 语义
旧语义是:
- 每个 slot 首次写入后永久锁死
新语义改成:
- finalize 前:允许 bootstrap admin 修正配置
- finalize 后:永久冻结
这更贴近真实部署流程,因为部署阶段允许纠错,而不是一次误配置直接锁死。
5. 为老版本 proxy 升级补充 migrateLegacyBootstrap()
仅仅把 bootstrapAdmin 和 bootstrapFinalized 追加到存储布局里,并不足以兼容老版本 proxy 升级。
原因是:
- 老 proxy 升级后,新变量默认值会是
bootstrapAdmin = 0 - 老 proxy 不会重新执行
initialize() - 如果不补 migration,新逻辑会停留在“没有 bootstrap admin、但也没有 finalized”的中间状态
因此本次新增:
migrateLegacyBootstrap()
该函数的设计原则是:
- 使用
reinitializer(2),确保 legacy migration 只能执行一次 - 不接受任何参数,避免升级治理只校验 implementation 地址时引入额外歧义
- 要求老 proxy 的模块地址已经完整配置
- 执行后直接将
bootstrapFinalized = true
这意味着老版本 SourceDao proxy 升级到新实现后,会直接进入“bootstrap 已完成”的状态,而不是重新进入可配置状态。
新部署与老版本升级的区别
新部署场景
对于全新部署的 SourceDao:
- 部署 proxy
- 调用
initialize() - 由
bootstrapAdmin设置各模块地址 - 配置完成后调用
finalizeInitialization()
这是标准的新版本 bootstrap 流程。
老版本 proxy 升级场景
对于已经在运行中的旧版 SourceDao proxy:
- proxy 地址保持不变
- 各模块 proxy 地址也通常保持不变
- 升级后不需要重新调用
setDevTokenAddress(...)/setCommitteeAddress(...)等接口 - 需要在升级时同步调用
migrateLegacyBootstrap()
也就是说,老版本升级的正确方式不是:
upgradeToAndCall(newImplementation, "0x")
而是:
upgradeToAndCall(
newImplementation,
abi.encodeCall(SourceDao.migrateLegacyBootstrap, ())
)
为什么老版本升级后不需要重新 set 模块地址
这里需要特别说明:
- 当前各核心模块采用的是 proxy 升级模式
- 模块升级通常只更换 implementation
- 模块 proxy 地址本身不会变化
因此:
DevToken升级时,DevTokenproxy 地址不会变化Committee升级时,Committeeproxy 地址不会变化Project、Dividend、Lockup、Acquired同理
SourceDao 存储的是模块 proxy 地址,而不是 implementation 地址。
所以在正常升级路径里,SourceDao 不需要重新设置这些模块地址。
migrateLegacyBootstrap() 的职责不是“重新配置模块”,而是把老版本 proxy 的 bootstrap 状态迁移成“已完成”。
升级注意事项
1. 必须使用 upgradeToAndCall(..., migrateLegacyBootstrap())
如果老版本 SourceDao proxy 直接升级到新实现,但没有调用 migrateLegacyBootstrap(),就会出现:
bootstrapAdmin == 0bootstrapFinalized == false
这会导致 bootstrap 状态不完整,不符合新实现预期。
2. 仅适用于模块地址已完整配置的老实例
migrateLegacyBootstrap() 要求老版本 SourceDao 的模块地址已经完整配置完成。
也就是说,这条 migration 适用于:
- 老系统已经完成部署并正常运行
- 只是现在要升级到新的
SourceDao实现
如果某个老实例本身模块地址还没配完,那么当前 migration 方案并不适用,需要单独设计迁移策略。
3. 不要把 migration 设计成带参数
本次 migration 故意不接受任何参数。
原因是当前升级治理校验只绑定“新 implementation 地址”,没有额外校验 upgradeToAndCall 的 calldata。
如果 migration 允许传入 admin 之类的参数,会引入治理执行层面的歧义和风险。
所以这里采用的是:
- 无参数 migration
- 自动校验旧状态
- 自动完成 finalize
这是当前治理模型下更安全的做法。
兼容性说明
存储布局
本次新增的状态变量:
bootstrapAdminbootstrapFinalized
都追加在 SourceDao 原有状态变量之后,没有重排已有变量顺序。
因此从 storage layout 角度看,proxy 升级是兼容的。
ABI
本次新增接口:
bootstrapAdmin()bootstrapFinalized()finalizeInitialization()migrateLegacyBootstrap()
原有 getter 和 setXAddress(...) 的函数签名没有变化。
行为变化
本次最核心的行为变化有三点:
- 模块地址不再能由任意 caller 抢写
- 模块地址不再接受 EOA
- bootstrap 语义从“set once”改成“部署阶段可修正,完成后冻结”
测试覆盖
test/dao.ts
验证:
bootstrapAdmin和bootstrapFinalized初始状态- fresh deploy 不能错误调用
migrateLegacyBootstrap() - zero address / EOA 地址被拒绝
- 非 bootstrap admin 不能配置模块,也不能 finalize
- bootstrap 阶段允许修正地址
- 未配置完整时不能 finalize
- finalize 后所有 bootstrap 配置入口永久关闭
isDAOContract(...)仍能正确识别模块地址
test/dev.ts / test/token.ts
验证:
- 在模块地址必须为真实合约的前提下,
DevToken/NormalToken原有授权和转账路径不受破坏 - 原先用 signer 地址伪装模块的测试夹具已改成最小 mock 合约
test/upgrade.ts
验证:
- 老版本
SourceDaoproxy 在模块地址完整配置的前提下,可以通过upgradeToAndCall(..., migrateLegacyBootstrap())升级到新实现 - 升级后 proxy 地址不变
- 升级后旧模块地址不变
- 升级后
bootstrapAdmin == 0 - 升级后
bootstrapFinalized == true - 升级后不需要重新
setXXX - 升级后 bootstrap 配置入口会正确拒绝后续修改
当前结论
这次 SourceDao 改动,本质上是把“开放式初始化”收紧成了两条清晰路径:
- 新部署:
bootstrap admin + finalize - 老版本升级:
upgradeToAndCall(..., migrateLegacyBootstrap())
它解决了部署阶段最直接的两个风险:
- 未初始化 slot 被任意地址抢写
- EOA 被误写入正式模块地址
同时也明确了老版本 proxy 升级到新实现时的正确迁移方式,避免出现“存储布局兼容,但运行时 bootstrap 状态未完成迁移”的问题。
Metadata
Metadata
Assignees
Labels
Type
Projects
Status