Skip to content

SourceDao 初始化收口与老版本升级兼容说明:bootstrap admin / finalize / migrateLegacyBootstrap #15

@lurenpluto

Description

@lurenpluto

背景

本次改动聚焦 SourceDao 的初始化部署阶段收口,以及老版本 SourceDao proxy 升级到新实现时的兼容处理。

在旧实现中,SourceDao 的模块地址初始化存在两个核心问题:

  1. 任意地址都可以抢先写入未初始化的模块 slot
  2. 模块 slot 只要求非零地址,不要求目标地址是合约,EOA 也能被写入正式模块地址

这两个问题都发生在 bootstrap 阶段,因此本次不是用长期治理权限去处理,而是引入一套短生命周期的初始化控制模型。


本次改动目标

本次改动的目标有五个:

  1. 为模块地址初始化建立明确的 bootstrap 权限边界
  2. 阻止 EOA 被写入正式模块 slot
  3. 允许部署阶段修正配置错误
  4. 在 bootstrap 完成后显式冻结配置入口
  5. 兼容老版本 SourceDao proxy 升级到新实现

具体改动

1. 引入 bootstrap admin

SourceDao.initialize() 现在会记录:

  • bootstrapAdmin = msg.sender
  • bootstrapFinalized = false

后续所有 setXAddress(...) 都要求:

  • 调用者必须是 bootstrapAdmin
  • 当前必须尚未 finalize

也就是说,模块地址不再能被任意 caller 抢先写入。

2. 模块地址校验收紧

模块地址现在不再只检查非零,而是要求:

  • 地址不为 0
  • code.length > 0

这样正式模块 slot 只接受合约地址,不再接受 EOA。

3. 引入 finalizeInitialization()

新增 finalizeInitialization()

只有在全部核心模块地址都已配置完成后,bootstrapAdmin 才能调用该函数。
一旦 finalize 成功:

  • bootstrapFinalized = true
  • 所有 setXAddress(...) 永久关闭
  • SourceDao bootstrap 阶段结束

4. 调整原来的 “set once” 语义

旧语义是:

  • 每个 slot 首次写入后永久锁死

新语义改成:

  • finalize 前:允许 bootstrap admin 修正配置
  • finalize 后:永久冻结

这更贴近真实部署流程,因为部署阶段允许纠错,而不是一次误配置直接锁死。

5. 为老版本 proxy 升级补充 migrateLegacyBootstrap()

仅仅把 bootstrapAdminbootstrapFinalized 追加到存储布局里,并不足以兼容老版本 proxy 升级。

原因是:

  • 老 proxy 升级后,新变量默认值会是 bootstrapAdmin = 0
  • 老 proxy 不会重新执行 initialize()
  • 如果不补 migration,新逻辑会停留在“没有 bootstrap admin、但也没有 finalized”的中间状态

因此本次新增:

  • migrateLegacyBootstrap()

该函数的设计原则是:

  • 使用 reinitializer(2),确保 legacy migration 只能执行一次
  • 不接受任何参数,避免升级治理只校验 implementation 地址时引入额外歧义
  • 要求老 proxy 的模块地址已经完整配置
  • 执行后直接将 bootstrapFinalized = true

这意味着老版本 SourceDao proxy 升级到新实现后,会直接进入“bootstrap 已完成”的状态,而不是重新进入可配置状态。


新部署与老版本升级的区别

新部署场景

对于全新部署的 SourceDao

  1. 部署 proxy
  2. 调用 initialize()
  3. bootstrapAdmin 设置各模块地址
  4. 配置完成后调用 finalizeInitialization()

这是标准的新版本 bootstrap 流程。

老版本 proxy 升级场景

对于已经在运行中的旧版 SourceDao proxy:

  1. proxy 地址保持不变
  2. 各模块 proxy 地址也通常保持不变
  3. 升级后不需要重新调用 setDevTokenAddress(...) / setCommitteeAddress(...) 等接口
  4. 需要在升级时同步调用 migrateLegacyBootstrap()

也就是说,老版本升级的正确方式不是:

upgradeToAndCall(newImplementation, "0x")

而是:

upgradeToAndCall(
    newImplementation,
    abi.encodeCall(SourceDao.migrateLegacyBootstrap, ())
)

为什么老版本升级后不需要重新 set 模块地址

这里需要特别说明:

  • 当前各核心模块采用的是 proxy 升级模式
  • 模块升级通常只更换 implementation
  • 模块 proxy 地址本身不会变化

因此:

  • DevToken 升级时,DevToken proxy 地址不会变化
  • Committee 升级时,Committee proxy 地址不会变化
  • ProjectDividendLockupAcquired 同理

SourceDao 存储的是模块 proxy 地址,而不是 implementation 地址。
所以在正常升级路径里,SourceDao 不需要重新设置这些模块地址。

migrateLegacyBootstrap() 的职责不是“重新配置模块”,而是把老版本 proxy 的 bootstrap 状态迁移成“已完成”。


升级注意事项

1. 必须使用 upgradeToAndCall(..., migrateLegacyBootstrap())

如果老版本 SourceDao proxy 直接升级到新实现,但没有调用 migrateLegacyBootstrap(),就会出现:

  • bootstrapAdmin == 0
  • bootstrapFinalized == false

这会导致 bootstrap 状态不完整,不符合新实现预期。

2. 仅适用于模块地址已完整配置的老实例

migrateLegacyBootstrap() 要求老版本 SourceDao 的模块地址已经完整配置完成。

也就是说,这条 migration 适用于:

  • 老系统已经完成部署并正常运行
  • 只是现在要升级到新的 SourceDao 实现

如果某个老实例本身模块地址还没配完,那么当前 migration 方案并不适用,需要单独设计迁移策略。

3. 不要把 migration 设计成带参数

本次 migration 故意不接受任何参数。

原因是当前升级治理校验只绑定“新 implementation 地址”,没有额外校验 upgradeToAndCall 的 calldata。
如果 migration 允许传入 admin 之类的参数,会引入治理执行层面的歧义和风险。

所以这里采用的是:

  • 无参数 migration
  • 自动校验旧状态
  • 自动完成 finalize

这是当前治理模型下更安全的做法。


兼容性说明

存储布局

本次新增的状态变量:

  • bootstrapAdmin
  • bootstrapFinalized

都追加在 SourceDao 原有状态变量之后,没有重排已有变量顺序。
因此从 storage layout 角度看,proxy 升级是兼容的。

ABI

本次新增接口:

  • bootstrapAdmin()
  • bootstrapFinalized()
  • finalizeInitialization()
  • migrateLegacyBootstrap()

原有 getter 和 setXAddress(...) 的函数签名没有变化。

行为变化

本次最核心的行为变化有三点:

  1. 模块地址不再能由任意 caller 抢写
  2. 模块地址不再接受 EOA
  3. bootstrap 语义从“set once”改成“部署阶段可修正,完成后冻结”

测试覆盖

test/dao.ts

验证:

  • bootstrapAdminbootstrapFinalized 初始状态
  • 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

验证:

  • 老版本 SourceDao proxy 在模块地址完整配置的前提下,可以通过 upgradeToAndCall(..., migrateLegacyBootstrap()) 升级到新实现
  • 升级后 proxy 地址不变
  • 升级后旧模块地址不变
  • 升级后 bootstrapAdmin == 0
  • 升级后 bootstrapFinalized == true
  • 升级后不需要重新 setXXX
  • 升级后 bootstrap 配置入口会正确拒绝后续修改

当前结论

这次 SourceDao 改动,本质上是把“开放式初始化”收紧成了两条清晰路径:

  1. 新部署:bootstrap admin + finalize
  2. 老版本升级:upgradeToAndCall(..., migrateLegacyBootstrap())

它解决了部署阶段最直接的两个风险:

  • 未初始化 slot 被任意地址抢写
  • EOA 被误写入正式模块地址

同时也明确了老版本 proxy 升级到新实现时的正确迁移方式,避免出现“存储布局兼容,但运行时 bootstrap 状态未完成迁移”的问题。

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