状态:Discussion / 未决
适用范围:
SourceDaoCommittee 的 full proposal 目前保留了“分批结算”设计:
- 提案到期后,由外部多次调用
endFullPropose(...) - 每次调用传入一批 voter 地址
- 合约逐批累加
agree / reject / settled
这样设计的原因是合理的:
- full proposal 的 voter 数量理论上可能很多
- 如果强制一次性结算全部 voter,单笔交易可能因为 gas 过高而失败
- 因此必须允许外部分批 settle
当前实现已经完成了一层收口:
- zero-balance outsider 已不能再对 full proposal 投票
- full proposal 不再需要额外 settle 这些零票重 outsider 记录
但还保留一个更深层的已知问题:
- full proposal 的票重是在
endFullPropose(...)结算时按“当前余额”计算 - 由于
endFullPropose(...)本身允许分批执行 - 同一份 token 在不同结算批次之间发生转移时,存在被重复计权的风险
问题不在“分批结算”本身,而在“分批结算时使用当前余额”。
举例:
- 地址
A和B都已经对同一 full proposal 投票 - 第一笔
endFullPropose(...)先结算A - 结算后
A把 token 转给B - 第二笔
endFullPropose(...)再结算B
如果每次都按结算当下余额计票,那么:
A会先按自己旧余额被计票B又会按收到 token 后的新余额被再次计票- 同一批 token 在一次 proposal 中被重复使用
从治理正确性角度看,这比“投票后中途转走导致票重变化”更关键,因为它会直接破坏“一份 token 在一次 vote 里只能算一次”的基本原则。
本议案希望讨论并最终确定以下目标:
- 保留 full proposal 的分批结算能力
- 让一次 full proposal 的所有 voter 都按同一个统一时点计权
- 防止同一批 token 在不同地址和不同 settle batch 之间被重复计算
- 尽量控制对现有合约、代理升级和日常 transfer gas 成本的影响
含义:
- 继续按
endFullPropose(...)执行当下的余额计票 - 不引入任何快照
优点:
- 不需要修改 token 合约
- 不需要新增 checkpoint 存储
- 兼容性成本最低
问题:
- 同一批 token 在不同 settle batch 之间可能被重复计权
- proposal 结果依赖结算顺序和结算时机
- 治理正确性不可严格保证
结论:
- 不建议继续长期保留
含义:
- 地址投票时立即记录该地址当时的票重
- 后续结算直接使用该地址已记录的票重
优点:
- 不再依赖结算时余额
endFullPropose(...)的分批结算仍然容易实现
问题:
- 只能解决“投票后余额变化影响自身票重”的问题
- 不能解决 token 转手后由另一个新地址继续投票的问题
- 同一批 token 仍然可能在多个地址之间被重复使用
结论:
- 不满足“同一批 token 在一次 vote 中只算一次”这个核心目标
- 不建议作为最终方案
含义:
- 为每个 full proposal 固定一个统一计票时点
- 所有 voter 在
endFullPropose(...)结算时,都按这个固定时点读取历史余额 - 继续允许分批 settle,但所有 batch 共用同一个快照时点
可选的快照时点:
- proposal 创建时
- proposal 截止时
proposal.expired
从当前 SourceDAO 语义出发,更推荐:
- 以
proposal.expired作为统一快照时点
原因:
- 当前 full proposal 本来就是按截止时间结束投票
- 在截止前 token 仍可自由流动
- 用截止时点作为统一计票时点,更符合“截止时谁持仓,谁对最终计票负责”的思路
优点:
- 分批结算仍然保留
- 同一 proposal 的所有 batch 都按同一时点计权
- 同一批 token 在不同地址之间转移后,不会因为不同 settle batch 被重复计权
问题:
- 需要 token 层支持历史余额查询
- 需要为
devRatio提供历史读取能力 - 需要接受日常 token 转账 gas 成本增加
结论:
- 这是当前最符合治理正确性的方向
- 推荐作为后续正式方案继续细化
推荐采用:方案 C:proposal 级统一快照时点
建议语义:
- full proposal 仍允许任意数量的
endFullPropose(...)批次 - 但每个 voter 的票重统一按
proposal.expired时刻的历史余额计算 agree / reject / threshold全部基于这个统一时点
在 contracts/DevToken.sol 和 contracts/NormalToken.sol 中增加:
- 账户余额的历史 checkpoint
totalSupply的历史 checkpoint
建议对外提供:
getPastBalance(address account, uint64 timestamp)getPastTotalSupply(uint64 timestamp)
实现原则:
- 账户余额变化时写 checkpoint
- 如果同一
timestamp已有最后一条记录,则覆盖而不是重复 append - 只在真实余额变化时写记录
full proposal 的票重不只是 token 数量,还要考虑 devRatio。
因此需要在 contracts/Committee.sol 中增加:
devRatio的 checkpointgetPastDevRatio(uint64 timestamp)
写入时机:
initialize(...)setDevRatio(...)
建议在 contracts/Committee.sol 中为每个 full proposal 追加缓存:
snapshotTimesnapshotRatiototalEligibleWeight
推荐做法:
- 第一次进入
endFullPropose(...)时初始化 - 后续 batch 直接复用
- 不修改现有
ProposalExtra结构体,避免 ABI 和存储布局风险
新的计票逻辑建议为:
snapshotTime = proposal.expiredsnapshotRatio = snapshotTime时刻生效的 ratiovoterWeight = normalPastBalance + devPastBalance * snapshotRatio / 100
threshold 所使用的总票基数也按同一快照时点计算。
当前 Committee 对 full proposal 有一条特殊语义:
- 如果最终版本已经发布,则
devRatio应锁到finalRatio
在统一快照模型下,推荐语义为:
- 如果主项目最终版本的发布时间
<= proposal.expired - 则该 full proposal 的有效 ratio 直接取
finalRatio - 否则取
proposal.expired时刻生效的历史devRatio
这样可以保证:
- full proposal 的 ratio 语义也和统一快照时点保持一致
这部分需要明确讨论。
- 可以保留
Committee的分批结算模式 - 不需要改变 full proposal 的对外执行入口
DevToken和NormalToken需要新增持久化 checkpoint 存储- 每次 token 余额变化的 gas 会增加
- 需要重新审查 upgradeable proxy 的存储布局追加位置
- 需要补充 legacy proxy 升级后的兼容测试
如果后续决定落地,至少应补以下测试:
- 同一批 token 在两个 voter 之间转移,但分两批结算时只应计一次
- voter 投票后在截止前转走 token,最终票重应按截止时余额而不是投票时余额
- voter 投票后在截止前收到 token,最终票重应按截止时余额计算
- final release 发生在 proposal 生命周期内时,结算应自动按
finalRatio - token checkpoint 升级后,旧 proxy 的余额和功能不应被破坏
- finalized 系统中 full proposal 换届、升级、项目治理链路都应继续通过
这份草案建议委员会后续重点讨论以下问题:
- 是否认可“full proposal 的统一计票时点应为
proposal.expired” - 是否接受 token 层增加 checkpoint 带来的长期 gas 成本
- 是否接受这次只解决“重复计权”问题,而不同时重做 proposer eligibility 等更大范围机制
- 是否要把该改动作为单独升级批次,而不是和其它治理变更打包
在正式决议前,建议把这项问题视为:
- 已识别的治理正确性问题
- 不应在没有统一方案的情况下继续零散修补
当前建议动作:
- 先保留本文档作为待讨论议案
- 在委员会内部确认是否接受“截止时统一快照”方向
- 若方向确认,再进一步细化具体状态变量、接口、升级步骤和测试计划