Skip to content

SourceDaoCommittee 普通提案快照治理改动说明:原因、收益与升级兼容性 #14

@lurenpluto

Description

@lurenpluto

背景

本次改动聚焦 SourceDaoCommittee 的普通提案治理语义修正,以及对应的升级兼容性处理。

在改动前,Committee 合约的普通提案存在一个核心问题:

  • 普通提案在结算时直接读取“当前委员会成员列表”
  • 这意味着如果委员会在提案创建后、结算前发生改组,旧提案的有效投票集合和过半门槛会被追溯性改变
  • 同时,普通提案的 support/reject 也没有绑定到提案创建时的委员会集合

这会导致治理语义不稳定:同一个提案,可能因为后续委员会成员变动而得到与创建时预期不同的结果。

本次改动的目标

本次改动的目标是:

  • 让普通提案在创建时绑定固定的委员会版本快照
  • 让普通提案的投票资格只认该快照中的成员
  • 让普通提案的结算门槛只认该快照中的委员会人数
  • 在不破坏已部署 proxy 存储布局的前提下完成升级

具体改动

1. 为普通提案引入委员会版本快照

新增了以下状态变量:

  • committeeVersion
  • proposalCommitteeVersion
  • committeeSizeByVersion
  • committeeMemberByVersion

普通 proposal 在创建时会绑定当前 committeeVersion,后续结算和投票资格都基于这个版本进行判断。

2. 普通提案投票资格改为按快照成员校验

support(...)reject(...) 的普通 proposal 路径现在会校验:

  • 投票人是否属于该 proposal 绑定的委员会版本

这意味着:

  • 后续新加入的委员,不能对旧普通提案补票
  • 不在快照中的 outsider,不能再对普通提案投票
  • 已经被移出当前委员会、但属于旧快照的成员,仍然可以完成旧普通提案的投票

3. 普通提案结算改为按快照版本计票

_settleProposal(...) 不再使用当前 committees 进行普通提案结算,而是:

  • 读取 proposal 绑定的 committeeVersion
  • 只统计该版本中有效成员的 support/reject
  • 以该版本的委员会人数作为过半门槛

这样可以确保普通提案的治理结果不会被后续委员会改组追溯性改写。

4. 委员会成员变更后推进快照版本

在以下路径成功执行后,会生成新的委员会快照版本:

  • addCommitteeMember
  • removeCommitteeMember
  • setCommittees

这样后续新 proposal 会绑定新的成员版本,而旧 proposal 仍绑定旧版本。

5. 为升级场景增加延迟初始化兜底

考虑到历史已部署的 proxy 不包含这些新快照变量,本次没有依赖额外 reinitializer,而是在普通 proposal 路径中加入了延迟初始化逻辑。

效果是:

  • 旧 proxy 升级到新实现后,新增快照变量初始为默认值
  • 当第一次进入普通 proposal 路径时,会自动完成快照初始化
  • 不需要为了这次升级单独执行一轮 reinitializer

这样做的好处

本次改动带来的主要收益有:

  • 普通提案的治理语义更稳定,结果不再受后续委员会改组污染
  • 普通提案的投票资格边界更清晰
  • 旧提案和新提案的治理上下文被正确隔离
  • 升级路径对历史 proxy 更友好,不需要额外的人工迁移步骤
  • 后续继续治理演进时,普通 proposal 的行为基础更可靠

升级兼容性说明

这次改动特别关注了升级兼容性,主要做了两件事。

1. 新增状态变量全部追加到原有变量尾部

最初设计时,如果把新增状态变量插入到已有变量中间,会导致 upgradeable proxy 的存储布局错位。

本次已经明确调整为:

  • 新增变量全部追加在 SourceDaoCommittee 原有状态变量末尾
  • 不重排已有变量
  • 不改变旧变量的 slot 顺序

这保证了已部署 proxy 在升级后不会因为 slot 偏移而读错旧数据。

2. 增加了真实的升级兼容测试

除了常规的“升级成功”测试之外,本次新增了一条更关键的兼容测试:

  • 使用旧布局的 legacy committee 实现部署 proxy
  • 在升级前先写入普通 proposal、full proposal、治理参数等旧状态
  • 再升级到包含 snapshot 逻辑的新实现
  • 升级后继续读取、投票、结算这些旧状态

这条测试的目的不是只验证 version() 变化,而是验证:

  • 旧状态在升级后仍可正确读取
  • 旧普通 proposal 仍可继续投票并结算
  • 旧 full proposal 仍可继续 endFullPropose(...)
  • 升级后新增 snapshot 状态不会破坏旧 proxy 的存储布局

验证情况

目前相关验证包括:

  • Committee 普通提案快照语义测试
  • 成员新增/移除后对旧普通提案继续投票的测试
  • fullPropose 参数校验测试
  • 旧布局 proxy 升级到新实现后的存储兼容测试
  • 全量测试回归

当前结果:

  • Committee 相关测试通过
  • upgrade 相关测试通过
  • 全量测试通过
  • 目前还有 1 pending,这是为未来 full proposal voter eligibility hardening 预留的待启用测试,不是失败用例

本次未覆盖范围

本次改动只处理普通 proposal,不改变 full proposal 的核心结算模型。

也就是说,以下问题本次仍然保留,后续需要单独治理收口:

  • full proposal 仍允许 zero-balance outsider 进入投票集合
  • full proposal 仍按结算时余额计算权重,而不是按投票时快照
  • full proposal 的 voter eligibility 仍未像普通 proposal 一样被正式收紧

结论

本次 Committee 合约改动,本质上是对普通提案治理语义的一次“纠偏”:

  • 让普通提案回到“按创建时委员会快照治理”的合理模型
  • 同时确保这次改动可以安全用于升级已有 proxy
  • 并通过升级兼容测试验证存储布局和历史状态不会被破坏

如果委员会成员认可这次方向,后续建议把 full proposal 的 voter eligibility 和权重快照问题继续纳入下一轮治理收口。

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