十三、从源码讲解compound v2 Governance:COMP / GovernorAlpha / Timelock

tomenengr 发布于 2026-05-06 阅读 184

第十三讲继续:Governance:COMP/GovernorAlpha/Timelock。前面我们讲代理升级时说过一句很关键的话:可升级协议的真正安全边界,不只是代码本身,还包括谁有权升级代码、谁有权改参数、升级前有没有延迟。Compoundv2的答案就是:COMP代币

第十三讲继续:Governance:COMP / GovernorAlpha / Timelock

前面我们讲代理升级时说过一句很关键的话:

可升级协议的真正安全边界,不只是代码本身,
还包括谁有权升级代码、谁有权改参数、升级前有没有延迟。

Compound v2 的答案就是:

COMP 代币
GovernorAlpha
Timelock

这一讲我们把 Compound v2 从“借贷协议”推进到“链上治理协议”。


第十三讲:Governance 治理源码精读

简介

本文精读 Compound v2 的治理系统,梳理 COMPGovernorAlphaTimelock 如何共同完成投票权委托、提案创建、投票、排队和延迟执行。读完可以理解 checkpoints 为什么能防止重复投票,提案的 targets / signatures / calldatas 才是真正执行内容,以及治理权限如何成为协议升级和参数调整的安全边界。

1. 为什么 Compound v2 需要治理?

Compound v2 里很多参数会影响整个协议安全:

collateralFactor
closeFactor
liquidationIncentive
reserveFactor
interestRateModel
priceOracle
pauseGuardian
Comptroller implementation
cToken implementation
新增市场
暂停市场操作

这些参数不能随便给某个私钥控制。 否则这个私钥一旦作恶或被盗,协议可能瞬间爆炸。

所以 Compound 后续把核心权限交给治理系统。

治理系统大概负责:

谁能提案
谁能投票
投票怎么算
提案通过后怎么排队
提案什么时候执行
执行时调用哪些合约

2. 三个核心组件

Compound v2 治理主线有三个合约:

COMP
GovernorAlpha
Timelock

它们分工是:

COMP:
  治理代币,记录投票权

GovernorAlpha:
  提案与投票系统

Timelock:
  延迟执行器,真正持有协议管理权限

关系图:

COMP holders
    |
    | delegate votes
    v
GovernorAlpha
    |
    | proposal passed
    v
Timelock
    |
    | delay
    v
execute target calls
    |
    v
Comptroller / cToken / Oracle / InterestRateModel / etc.

一句话:

COMP 决定谁有票;
GovernorAlpha 决定提案是否通过;
Timelock 决定通过后的提案什么时候真正执行。

3. 治理不是直接调用协议

很多人一开始会误解:

提案通过后,是 GovernorAlpha 直接改 Comptroller 参数吗?

不是。

通常是:

GovernorAlpha 把通过的操作 queue 到 Timelock
Timelock 等待 delay
Timelock 执行具体调用

为什么要 Timelock?

因为治理提案可能非常危险,比如:

更换 Comptroller implementation
更换 PriceOracle
把某资产 collateralFactor 调到很高
升级 cToken implementation

如果提案一通过就立刻执行,用户没有逃跑窗口。

Timelock 给市场一个缓冲时间:

大家可以看到即将执行什么;
如果觉得危险,可以退出或反应。

4. COMP 是什么?

COMP 是 Compound 的治理代币。

它不是借贷市场里的 cToken。 它的核心用途是治理投票。

COMP 合约通常是一个 ERC20,但比普通 ERC20 多了治理投票相关逻辑:

delegate
delegateBySig
getCurrentVotes
getPriorVotes
checkpoints

普通 ERC20 只关心:

谁有多少 token

治理代币还要关心:

谁有多少投票权
某个历史区块时谁有多少投票权
用户把投票权委托给了谁

5. 为什么需要 delegate?

持有 COMP 不等于自动用自己的地址投票。

用户需要把投票权委托给某个地址:

delegate(address delegatee)

可以委托给自己:

Alice 持有 COMP
Alice 调用 delegate(Alice)
Alice 自己获得投票权

也可以委托给别人:

Alice 持有 COMP
Alice 调用 delegate(Bob)
Bob 获得 Alice 那部分投票权

注意:

delegate 转移的是投票权,不是 token 所有权。

Alice 的 COMP 还在 Alice 钱包里,只是投票权归 Bob 使用。


6. 为什么不让 token balance 直接等于 votes?

因为治理要支持委托。

很多 token 持有人不想自己研究提案,但愿意把票权交给代表。

比如:

普通用户把 COMP 委托给治理代表
基金把 COMP 委托给安全委员会
团队成员委托给某个多签

所以治理投票权不是简单:

votes = balanceOf[user]

而是:

votes = 被委托给这个地址的 COMP 总量

源码里会维护:

mapping(address => address) public delegates;

表示:

delegates[delegator] = delegatee

7. checkpoints:为什么需要历史投票权?

治理投票最怕一个问题:

用户在投票期间来回转 token,重复投票怎么办?

举个攻击:

Alice 有 100 COMP
Alice 给提案投票
Alice 把 100 COMP 转给 Bob
Bob 再投票
Bob 转给 Carol
Carol 再投票

如果投票权按实时余额算,就可以刷票。

所以治理系统必须使用某个固定区块的投票权快照。

Compound 的做法是 checkpoint。

用户投票权变化时,写入检查点:

struct Checkpoint {
    uint32 fromBlock;
    uint96 votes;
}

每个地址有一组 checkpoints:

mapping(address => mapping(uint32 => Checkpoint)) public checkpoints;
mapping(address => uint32) public numCheckpoints;

意思是:

在某个区块,这个地址的投票权是多少。

8. getPriorVotes

投票时经常用:

getPriorVotes(address account, uint blockNumber)

它返回:

account 在 blockNumber 那个区块时的投票权

这是治理系统的核心。

提案创建时会确定一个:

startBlock

投票时看的是:

用户在 startBlock 或某个历史区块的投票权

而不是当前余额。

这样用户就不能在投票期间搬 token 刷票。


9. checkpoint 如何更新?

当 COMP 转账、mint、burn 或 delegate 变化时,投票权可能变化。

比如 Alice 把投票权委托给 Bob:

Alice delegatee 从 Alice 变成 Bob

那么:

Alice 原 delegatee 的 votes 减少
Bob 的 votes 增加

源码里会有类似:

_moveDelegates(srcRep, dstRep, amount)

逻辑:

如果 srcRep 不是零地址:
  srcRep votes -= amount
  写 checkpoint

如果 dstRep 不是零地址:
  dstRep votes += amount
  写 checkpoint

所以 token 转移时,如果转出方和接收方的 delegatee 不同,也要移动投票权。


10. delegateBySig

COMP 还支持:

delegateBySig(...)

意思是:

用户不用自己发链上交易,
只需要签名;
别人可以替他提交委托交易。

这类似 EIP-712 风格的签名授权。

作用是降低治理参与门槛:

用户可以离线签名,把投票权委托出去;
提交签名的人帮他付 gas。

这在治理系统里很常见。


11. GovernorAlpha 是什么?

GovernorAlpha 是提案和投票合约。

它负责:

创建提案
记录提案状态
记录投票
判断投票是否通过
把通过的提案 queue 到 Timelock
执行已排队且到期的提案
取消恶意或失效提案

核心结构通常是:

struct Proposal {
    uint id;
    address proposer;
    uint eta;
    address[] targets;
    uint[] values;
    string[] signatures;
    bytes[] calldatas;
    uint startBlock;
    uint endBlock;
    uint forVotes;
    uint againstVotes;
    bool canceled;
    bool executed;
    mapping(address => Receipt) receipts;
}

这个结构很重要,我们拆开看。


12. Proposal 里有什么?

一个提案不是一句话,而是一组链上调用。

比如提案要做:

把 cETH collateralFactor 从 75% 调到 80%
把 cDAI reserveFactor 从 10% 调到 12%
更换某市场 InterestRateModel

那它会包含一组 target call:

targets:
  [Comptroller, cDAI, cUSDC]

values:
  [0, 0, 0]

signatures:
  ["_setCollateralFactor(address,uint256)",
   "_setReserveFactor(uint256)",
   "_setInterestRateModel(address)"]

calldatas:
  [encoded args, encoded args, encoded args]

执行时,Timelock 会逐个调用这些 target。

所以治理提案本质是:

一批延迟执行的合约调用。

13. Proposal 字段解释

id:
  提案编号

proposer:
  提案发起人

eta:
  提案在 Timelock 中预计可执行的时间

targets:
  要调用的合约地址列表

values:
  每个调用附带的 ETH 数量

signatures:
  函数签名列表

calldatas:
  函数参数编码

startBlock:
  投票开始区块

endBlock:
  投票结束区块

forVotes:
  赞成票数量

againstVotes:
  反对票数量

canceled:
  是否已取消

executed:
  是否已执行

receipts:
  每个投票人的投票记录

14. Receipt:投票记录

每个用户对某个提案只能投一次。

所以有:

struct Receipt {
    bool hasVoted;
    bool support;
    uint96 votes;
}

含义:

hasVoted:
  是否已经投过票

support:
  支持还是反对

votes:
  这次投票使用了多少票权

投票后写入:

proposal.receipts[voter] = Receipt(...)

这样可以防止重复投票。


15. 创建提案:propose

提案入口通常是:

propose(
    address[] memory targets,
    uint[] memory values,
    string[] memory signatures,
    bytes[] memory calldatas,
    string memory description
)

不是任何人都能提案。 必须达到提案门槛:

proposalThreshold

也就是:

proposer 的历史投票权 >= proposalThreshold

为什么要门槛?

防止垃圾提案刷屏。 如果任何地址 0 votes 都能提案,治理会被 spam 打爆。


16. 为什么用历史 votes 判断 proposer?

创建提案时,也会看历史区块的投票权,而不是当前。

通常会用:

comp.getPriorVotes(msg.sender, block.number - 1)

意思是:

看 proposer 在上一个区块的投票权。

为什么不是当前区块?

因为当前区块内可能存在复杂顺序操纵。 用前一个区块更稳定。

如果票权不足:

不能创建提案。

17. proposalThreshold 和 quorumVotes

治理里有两个很重要的参数:

proposalThreshold:
  创建提案所需最低票权

quorumVotes:
  提案通过所需最低赞成票数量

区别:

proposalThreshold:
  防 spam,决定谁能提案

quorumVotes:
  防低参与度通过,决定提案是否有足够治理共识

一个提案想通过,通常需要:

forVotes > againstVotes
并且
forVotes >= quorumVotes

只比反对票多还不够,还要达到法定参与量。


18. 投票周期

创建提案后,不会立刻投票。

通常有:

votingDelay

例如:

提案创建后,等待若干区块才开始投票。

然后进入投票期:

votingPeriod

例如:

从 startBlock 到 endBlock。

状态流:

Pending
  -> Active
  -> Succeeded / Defeated

其中:

Pending:
  提案已创建,但还没到 startBlock

Active:
  正在投票

Succeeded:
  投票结束,赞成票足够且超过反对票

Defeated:
  投票失败

19. castVote

投票入口:

castVote(uint proposalId, bool support)

内部会:

1. 检查提案状态必须是 Active
2. 检查 voter 没有投过
3. 获取 voter 在 startBlock 的投票权
4. 根据 support 增加 forVotes 或 againstVotes
5. 写入 Receipt
6. emit VoteCast

关键是第 3 步:

uint96 votes = comp.getPriorVotes(voter, proposal.startBlock);

这确保投票权以提案开始时为准。


20. 为什么投票权不是投票瞬间余额?

还是为了防止刷票。

如果按实时余额:

Alice 投完转给 Bob
Bob 投完转给 Carol

同一批 COMP 可以重复投。

startBlock 快照后:

投票期间转账不会改变这个提案的投票权。

它可能影响未来提案,但不影响当前提案。


21. queue:把成功提案放进 Timelock

提案投票成功后,还不能立刻执行。

需要调用:

queue(uint proposalId)

它会把提案里的每个 action 放进 Timelock。

每个 action 由这些东西唯一确定:

target
value
signature
calldata
eta

其中:

eta = block.timestamp + timelock.delay()

也就是最早可执行时间。

queue 后,提案状态变成:

Queued

22. Timelock 是什么?

Timelock 是真正持有协议 admin 权限的合约。

比如 Comptroller 的 admin 可能是 Timelock,而不是 GovernorAlpha。

这意味着:

只有 Timelock 才能调用 Comptroller 的高权限函数。

例如:

Comptroller._setCollateralFactor(...)
Comptroller._setPriceOracle(...)
Unitroller._setPendingImplementation(...)
cToken._setReserveFactor(...)
cToken._setInterestRateModel(...)

GovernorAlpha 只是投票系统。 真正执行高权限调用的是 Timelock。


23. Timelock 的核心参数

Timelock 里通常有:

address public admin;
uint public delay;
mapping(bytes32 => bool) public queuedTransactions;

含义:

admin:
  谁能 queue / cancel / execute,一般是 GovernorAlpha

delay:
  排队后必须等待多久才能执行

queuedTransactions:
  哪些交易已经排队

Timelock 会限制:

交易必须先 queue
等待 delay
到 eta 后才能 execute
超过 grace period 可能过期

24. Timelock 的 queueTransaction

提案成功后,GovernorAlpha 调:

timelock.queueTransaction(
    target,
    value,
    signature,
    data,
    eta
)

Timelock 检查:

msg.sender 必须是 admin
eta 必须 >= 当前时间 + delay

然后生成 txHash:

txHash = keccak256(target, value, signature, data, eta)

并记录:

queuedTransactions[txHash] = true;

这表示:

这笔治理调用已经排队。

25. Timelock 的 executeTransaction

等到时间到了,GovernorAlpha 可以调用:

timelock.executeTransaction(
    target,
    value,
    signature,
    data,
    eta
)

Timelock 检查:

这笔交易已经 queued
当前时间 >= eta
当前时间 <= eta + gracePeriod

然后执行调用。

如果 signature 非空,会把函数选择器和 data 拼起来:

callData = abi.encodePacked(bytes4(keccak256(signature)), data)

然后:

target.call{value: value}(callData)

这就真正修改协议参数或升级实现。


26. execute:执行提案

GovernorAlpha 里的:

execute(uint proposalId)

会对提案里的每个 action 调 Timelock execute。

流程:

1. 检查提案状态是 Queued
2. 标记 proposal.executed = true
3. 遍历 targets / values / signatures / calldatas
4. 对每个 action 调 timelock.executeTransaction(...)

执行成功后,提案状态变成:

Executed

27. cancel:取消提案

提案可以被取消。

典型场景:

proposer 的票权跌破 proposalThreshold
提案还没执行

为什么 proposer 票权降低后可以取消?

为了防止某人短暂借票或获得委托后创建提案,之后失去治理支持,但提案仍继续推进。

cancel 会:

1. 标记 proposal.canceled = true
2. 取消 Timelock 中已 queue 的 action

如果提案还没 queue,就只标记 canceled。


28. proposal state 状态机

GovernorAlpha 通常有一个 state(proposalId) 函数。

常见状态:

Pending:
  当前区块 <= startBlock

Active:
  当前区块在 startBlock 和 endBlock 之间

Canceled:
  已取消

Defeated:
  投票结束,但反对票 >= 赞成票,或赞成票没达到 quorum

Succeeded:
  投票通过,但还没 queue

Queued:
  已进入 Timelock,等待执行

Expired:
  queue 后超过 gracePeriod 没执行

Executed:
  已执行

状态流大概是:

Pending
  -> Active
  -> Succeeded
  -> Queued
  -> Executed

Pending / Active / Succeeded / Queued
  -> Canceled

Active
  -> Defeated

Queued
  -> Expired

29. 治理如何升级 Comptroller?

现在把第十二讲和这一讲串起来。

如果治理想升级 Comptroller implementation,大致提案 action 可能是:

target: Unitroller
signature: "_setPendingImplementation(address)"
calldata: newComptrollerImplementation

执行后:

Unitroller.pendingComptrollerImplementation = newImpl

然后还需要新 implementation 接受:

target: newComptrollerImplementation
signature: "_become(address)"
calldata: Unitroller address

或者调用对应 _acceptImplementation 流程,具体看版本实现。

完整治理链路:

COMP holders vote
  -> GovernorAlpha proposal succeeds
  -> GovernorAlpha queues actions in Timelock
  -> delay passes
  -> Timelock calls Unitroller / Comptroller implementation
  -> Unitroller switches implementation

所以升级不是 admin 随手一改,而是治理延迟执行。


30. 治理如何修改市场参数?

比如治理想把某市场 collateralFactor 改成 80%。

提案 action 可能是:

target:
  Comptroller / Unitroller address

signature:
  "_setCollateralFactor(address,uint256)"

calldata:
  cToken address
  0.8e18

执行路径:

Timelock
  -> Unitroller
  -> delegatecall Comptroller implementation
  -> 修改 Unitroller storage 里的 markets[cToken].collateralFactorMantissa

看到了吗?

这里同时涉及:

GovernorAlpha
Timelock
Unitroller proxy
Comptroller implementation
Comptroller storage layout
markets mapping

DeFi 治理的一个参数修改,背后是一整套机器在转。


31. 治理如何更换利率模型?

某个 cToken 市场的利率模型可以通过治理修改。

提案 action 可能是:

target:
  cToken address, 例如 cDAI

signature:
  "_setInterestRateModel(address)"

calldata:
  newInterestRateModel address

执行路径:

Timelock
  -> cDAI Delegator
  -> delegatecall CErc20Delegate
  -> 修改 cDAI storage 中的 interestRateModel

然后之后 accrueInterest() 时就会调用新模型。

注意,通常修改前会先:

accrueInterest()

确保旧模型下利息先结清到当前区块,再切新模型。


32. 治理如何新增市场?

新增市场通常会涉及:

部署新的 cToken
设置 interestRateModel
设置 initialExchangeRate
设置 reserveFactor
在 Comptroller 中 supportMarket
设置 collateralFactor
设置 priceOracle 支持
配置 COMP 分发

治理 action 可能包括:

Comptroller._supportMarket(cToken)
Comptroller._setCollateralFactor(cToken, factor)
Comptroller._setCompSpeed(cToken, speed)
Oracle 设置资产价格来源

新增市场很敏感,因为如果:

价格源错了
collateralFactor 太高
underlying token 有恶意行为
transfer 逻辑不标准

都可能给协议带来风险。


33. Governance 的安全边界

治理系统本身有几个安全点:

proposalThreshold 防垃圾提案
quorumVotes 防低参与度通过
votingDelay 给市场观察提案时间
votingPeriod 给投票时间
Timelock delay 给执行前缓冲时间
checkpoints 防止重复投票
cancel 防止失去支持的 proposer 推进提案

但它也有风险:

大户或委托集中导致治理捕获
借贷市场可借到治理代币时可能出现治理攻击
恶意提案伪装成正常参数调整
Timelock delay 太短,用户反应不及
投票参与率低
前端展示不完整,用户看不懂 calldata

所以治理不是银弹。 它只是把“单点管理员风险”换成“公开治理与延迟执行风险”。


34. COMP 治理源码骨架

COMP
 |
 |-- balances
 |-- delegates[delegator] = delegatee
 |-- checkpoints[account][index]
 |-- numCheckpoints[account]
 |
 |-- delegate(delegatee)
 |-- delegateBySig(...)
 |-- getCurrentVotes(account)
 |-- getPriorVotes(account, blockNumber)
 |
 |-- _moveDelegates(srcRep, dstRep, amount)
 |-- _writeCheckpoint(delegatee, oldVotes, newVotes)

35. GovernorAlpha 源码骨架

GovernorAlpha
 |
 |-- comp
 |-- timelock
 |-- proposalCount
 |-- proposals[id]
 |-- latestProposalIds[proposer]
 |
 |-- propose(...)
 |-- castVote(proposalId, support)
 |-- queue(proposalId)
 |-- execute(proposalId)
 |-- cancel(proposalId)
 |-- state(proposalId)
 |
 |-- proposalThreshold()
 |-- quorumVotes()
 |-- votingDelay()
 |-- votingPeriod()

36. Timelock 源码骨架

Timelock
 |
 |-- admin
 |-- pendingAdmin
 |-- delay
 |-- queuedTransactions[txHash]
 |
 |-- queueTransaction(target, value, signature, data, eta)
 |-- cancelTransaction(target, value, signature, data, eta)
 |-- executeTransaction(target, value, signature, data, eta)
 |-- setDelay(delay)
 |-- setPendingAdmin(pendingAdmin)
 |-- acceptAdmin()

37. 一次完整治理流程

假设治理要把 cDAI reserveFactor 改成 15%。

流程:

1. Proposer 拥有足够 delegated COMP votes

2. Proposer 调用 GovernorAlpha.propose(...)
   target = cDAI
   signature = "_setReserveFactor(uint256)"
   calldata = 0.15e18

3. 提案进入 Pending

4. 到 startBlock 后进入 Active

5. COMP voters 调用 castVote

6. 到 endBlock 后:
   如果 forVotes > againstVotes
   且 forVotes >= quorumVotes
   提案变成 Succeeded

7. 任意人可调用 queue(proposalId)

8. GovernorAlpha 把 action queue 到 Timelock

9. 等待 delay

10. 任意人可调用 execute(proposalId)

11. Timelock 调用 cDAI._setReserveFactor(0.15e18)

12. cDAI 更新 reserveFactorMantissa

13. 之后 accrueInterest 时,新 reserveFactor 生效

38. 治理和用户的关系

普通用户最直接接触的是:

delegate 投票权
投票
查看提案
理解提案 calldata

协议开发者和安全研究员更关心:

提案调用了哪些 targets
修改了哪些参数
是否经过 Timelock
是否可能破坏 storage layout
是否修改 oracle / implementation / admin 权限
是否引入恶意市场

治理提案不能只看 description。 真正要看的是:

targets
signatures
calldatas
values

因为 description 可以写得很美,calldata 才是真动作。链上不会执行小作文,它只执行 bytes。


39. 第十三讲要记住的 8 个结论

第一,Compound v2 治理核心是:

COMP + GovernorAlpha + Timelock

第二,COMP 是治理代币,投票权通过 delegate 产生。

第三,checkpoints 记录历史投票权,防止投票期间转 token 重复投票。

第四,GovernorAlpha 负责提案、投票、排队和执行状态管理。

第五,Timelock 才是真正持有协议管理权限的延迟执行器。

第六,提案本质是一组链上调用:

targets + values + signatures + calldatas

第七,提案通过后必须先 queue 到 Timelock,等待 delay 后才能 execute。

第八,治理可以修改市场参数、升级实现、支持新市场、切换利率模型和预言机,因此治理权限本身就是协议安全边界。


下一讲进入 COMP 分发机制 / Flywheel

也就是:

为什么供应和借款会获得 COMP?
compSupplyState / compBorrowState 是什么?
index 如何累计奖励?
supplierIndex / borrowerIndex 怎么避免遍历用户?
为什么它和 borrowIndex 思想很像?
claimComp 怎么算用户未领取奖励?

这部分很有意思,因为它是另一套“全局 index + 用户 snapshot”的设计,和我们前面讲的 borrowIndex 有异曲同工之妙。

相关文章

0 条评论