JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

鏈上投票:代幣權重、法定門檻與時間鎖

一場鏈上投票,本質上是一台由智慧合約組成的狀態機。我們將透過標準的 Governor 框架,追蹤一份真實提案從草稿到執行的全程——過去區塊的快照、提案門檻、法定門檻、委託,以及那道讓所有人都來得及反應的時間鎖——再把它與免 gas 的鏈下投票,以及那些試圖逃離金權政治的更公平選票放在一起權衡。

從論壇討論串,到協定親手簽下的一筆交易

治理論壇上冒出一行字:GP-12:撥款給安全稽核——從金庫送出 100,000 GOV 給稽核團隊。 在一間普通公司裡,財務主管看一眼、核准,就把這筆轉帳簽了。但在 DAO 裡,沒有任何一個人握著那枝筆。金庫的鑰匙屬於一份 智慧合約,而唯一能讓它動用金錢的,是一場由合約自己清點、然後服從的投票。上一篇談到,一個 DAO 拆開來看,就是一座 金庫 加上一套寫成程式碼的規則。這一篇,我們要打開其中最關鍵的那條規則——這些代幣持有者,究竟是如何在鏈上、以機械的方式做出決定的。

幾乎每一個正經的 DAO,都採用同一套「三個盒子」的機器——由 Compound 的 Governor Bravo 與 OpenZeppelin 的 Governor 合約所發揚光大。第一個盒子是一枚 治理代幣,用來衡量每個地址的選票有多重。第二個盒子是 Governor 合約——一台狀態機,讓 提案 走完一套固定的生命週期並清點選票。第三個盒子是一道獨立的 時間鎖,它才是真正握有金庫的那一方,並且在一段強制延遲過去之前,拒絕執行任何東西。把這三個盒子分清楚,你日後讀到的每一場鏈上投票就都看得懂了。

投票權,是你過去持有的代幣——以及你把它借給了誰

第一個驚喜在這裡:在 代幣加權投票 裡,持有代幣跟「擁有選票」並不是同一回事。OpenZeppelin 的標準 `ERC20Votes` 要等你委託之後,才開始計算你的權重——哪怕你只是把票委託給自己。原因是成本:鏈承擔不起在每一筆轉帳時都重算所有人的餘額,所以它只為「有人委託過去」的地址,維護一份持續更新的投票權檢查點(checkpoint)

// OpenZeppelin v5 — a token whose balances can become votes.
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

contract GovToken is ERC20, ERC20Permit, ERC20Votes {
    constructor() ERC20("Gov", "GOV") ERC20Permit("Gov") {
        _mint(msg.sender, 1_000_000e18);
    }
    // ...required v5 _update / nonces overrides omitted for brevity...
}

// A balance counts as ZERO votes until it is delegated -- even to yourself:
//   token.delegate(myself);           // activate my own weight
//   token.delegate(trustedDelegate);  // lend my weight, keep custody
//   token.delegateBySig(...);         // do it gaslessly, with a signature

// Every transfer writes a checkpoint, so the chain can answer later:
//   token.getPastVotes(account, pastBlock)   // my votes at a past block
//   token.getPastTotalSupply(pastBlock)      // total votes back then
一枚 ERC20Votes 代幣:餘額要委託之後才成為選票,而歷史會被記成檢查點。

現在來看承重的關鍵細節。當一份提案開始時,Governor 會固定一個快照區塊,從此以後,它清點每位投票者的權重時,用的是 `getPastVotes(voter, snapshotBlock)`——也就是他們在那個過去區塊當下的權力,而非今天的餘額。光是這一個設計抉擇,就成了防堵租票的第一道防線。一筆 閃電貸 能在一筆交易的時間內塞給攻擊者數百萬枚代幣,卻無法竄改歷史檢查點,所以那些借來的代幣,對一個早已成為過去的快照而言,攜帶的投票權是。(完整的 治理攻擊 圖鑑,以及這道防線在哪裡仍會失守,是下一篇的主題。)

提案的生命週期,是一台狀態機

一份提案不是一句表達意圖的英文——它是一綑確切的呼叫,Governor 當且僅當投票通過時才會執行。你以三個平行的陣列加上一段給人看的說明來提交它:要呼叫的合約(`targets`)、每筆要附帶的 ETH(`values`),以及每筆精確編碼後的 calldata(`calldatas`)。Governor 會把這一切雜湊成一個決定性的 `proposalId`,所以同一組動作永遠產生同一個 id,而那段友善的說明文字,也永遠無法被偷偷掉包成不同的程式碼。

// GP-12: "send 100,000 GOV from the treasury to the audit team"
address[] memory targets   = new address[](1);
uint256[] memory values    = new uint256[](1);
bytes[]   memory calldatas = new bytes[](1);

targets[0]   = address(govToken);   // the call is made TO the token contract,
values[0]    = 0;                    // with no ETH attached,
calldatas[0] = abi.encodeCall(IERC20.transfer, (grantee, 100_000e18));
string memory description = "GP-12: fund the security audit grant";

uint256 id = governor.propose(targets, values, calldatas, description);

// The id is a deterministic hash of the EXACT actions plus the description:
//   keccak256(abi.encode(targets, values, calldatas, keccak256(bytes(description))))
// The funds live in the timelock, so the transfer is ultimately made BY the timelock.
提交一份提案:它將執行的鏈上呼叫,在建立的當下就已寫死,並雜湊進提案 id。
  1. 待議(Pending)。 提案已存在,但投票尚未開放。Governor 會等待 `votingDelay` 個區塊,而所有人投票權的快照,會在投票開始的那一刻拍下——這段延遲讓持有者有時間在權重凍結之前完成委託或移轉代幣。
  2. 進行中(Active)。 投票開放 `votingPeriod` 個區塊。每位投票者投下贊成、反對或棄權,權重取自快照當下的 `getPastVotes`。而且,當初只有持票達到 `proposalThreshold` 的地址,才有資格開啟這份提案——這道門檻就是防垃圾提案的閘門。
  3. 否決(Defeated)或通過(Succeeded)。 投票結束時,Governor 檢查兩個條件——法定門檻與多數(下一節說明)。任一不過,提案即否決、就此作廢;兩者皆過,提案即通過
  4. 排程(Queued)。 通過的提案被排程進時間鎖,強制延遲的時鐘從此開始倒數。此刻仍未執行任何動作——決定已經做出,但行動仍在等待。
  5. 已執行(Executed,或逾期 Expired)。 時間鎖延遲過後,任何人都能呼叫 `execute`,那綑呼叫終於發動。若在時間鎖的寬限期內無人執行,已排程的提案會逾期,必須重新提案。

這些旋鈕是具體的設定,不是抽象概念。以 Compound 正在運行的 Governor Bravo 為例,它的 `votingDelay` 是 13,140 個區塊(約 2 天)、`votingPeriod` 是 19,710 個區塊(約 3 天)、`proposalThreshold` 是 25,000 COMP、法定門檻則是 400,000 COMP。把這些數字讀成一種刻意安排的節奏:從點子到執行大約要一週,慢到足以讓代議者與一般使用者真的注意到、並在任何不可逆的事情發生前作出反應。

法定門檻與門檻值:是兩道關卡,不是一道

有三個各自不同的數字在替提案把關,而把它們混為一談是初學者的經典錯誤。提案門檻是你光要開啟一份提案就需要的權重——它是一筆防垃圾的費用,不是一個票數。另外兩個則在投票結束時檢查。第一個是 法定門檻:必須有足夠的總權重出席,好讓一小撮不具代表性的人,無法趁眾人熟睡時把事情通過。在 OpenZeppelin 的 `GovernorVotesQuorumFraction` 裡,法定門檻是快照當下投票供給的一個百分比——比方說 4%。第二個則是多數決測試本身。

// GovernorCountingSimple encodes each ballot as:
//   support = 0 (Against) | 1 (For) | 2 (Abstain)

// When voting closes, TWO independent gates are checked:
quorumReached = (forVotes + abstainVotes) >= quorum(snapshotBlock);
voteSucceeded = (forVotes > againstVotes);

passes = quorumReached && voteSucceeded;   // BOTH must hold

// The asymmetry that trips people up:
//   Abstain  -> counts toward QUORUM, but NOT toward For-beats-Against
//   For      -> counts toward both
//   Against  -> counts toward NEITHER quorum nor success
GovernorCountingSimple:法定門檻(贊成+棄權)與簡單多數(贊成>反對),是兩道各自獨立的關卡。

把它落到具體數字。假設總供給為 1,000,000 GOV、法定門檻為 4%——也就是必須有 40,000 的權重參與。一場投票以 贊成 55,000、反對 30,000、棄權 5,000 收場。法定門檻檢查:贊成+棄權 = 60,000 ≥ 40,000 ✓。多數檢查:55,000 > 30,000 ✓。提案通過。現在改變一下出席率:贊成 22,000、反對 3,000、棄權 5,000。多數壓倒性地懸殊(22,000 ≫ 3,000),但贊成+棄權 = 27,000 < 40,000,法定門檻未達,提案遭否決。一個受歡迎的點子,仍可能只因為來投票的人太少而胎死腹中。

時間鎖:任何事情發生前的一段強制暫停

為什麼要延遲一份已經贏了的提案?因為「程式即法律」是一把雙面刃:一份通過的提案能搬走整座金庫、或升級協定自己的合約,而事後沒有上訴法院可言。所以標準框架做了一件很巧妙的事——Governor 並不擁有金錢或管理權限。擁有它們的,是一份獨立的 `TimelockController`。Governor 只是握有那道時間鎖的提案者角色;它能排程動作、卻無法發動,而時間鎖在最短延遲走完之前,拒絕執行任何東西。

// The Timelock -- not the Governor -- holds the treasury and admin keys.
TimelockController timelock = new TimelockController(
    2 days,     // minDelay: nothing executes until 2 days after queuing
    proposers,  // [address(governor)] -> only the Governor may schedule
    executors,  // [address(0)]        -> after the delay, ANYONE may execute
    admin       // wires the roles; ideally renounced once set up
);

// Lifecycle of a winning proposal:
//   governor.queue(...)   -> timelock.schedule(...)  // start the 2-day clock
//   ...wait minDelay...                              // the public safety window
//   governor.execute(...) -> timelock.execute(...)   // the calls finally fire
TimelockController 擁有那些特權動作;Governor 只能把它們排程。
// A complete, standard Governor, assembled from OpenZeppelin v5 mixins.
contract MyGovernor is
    Governor, GovernorSettings, GovernorCountingSimple,
    GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl
{
    constructor(IVotes token, TimelockController timelock)
        Governor("MyGovernor")
        GovernorSettings(
            7200,    // votingDelay:  ~1 day  (blocks until the snapshot)
            50400,   // votingPeriod: ~1 week (blocks the poll stays open)
            100e18   // proposalThreshold: votes needed to OPEN a proposal
        )
        GovernorVotes(token)               // weight = token.getPastVotes(...)
        GovernorVotesQuorumFraction(4)     // quorum = 4% of past total supply
        GovernorTimelockControl(timelock)  // execution routes THROUGH the timelock
    {}
    // The mixins supply votingDelay(), votingPeriod(), quorum(timepoint),
    // proposalThreshold(), state(), propose(), castVote(), queue(), execute().
    // (Several v5 override stubs are elided here for brevity.)
}
每個基底合約貢獻遊戲規則中的一條;合起來,就是一份標準的 Governor。

這段等待的窗口,就是安全閥。在延遲期間——Compound 的時間鎖是 2 天——任何不滿意結果的人都能行動:使用者可以在變更生效前把資金提走、退出;而守護者(通常是一個握有時間鎖取消者角色的 多重簽章錢包)可以在惡意但已通過的提案執行之前否決它。這段延遲,把一場具約束力的投票,從一個瞬間、不可逆的事件,變成了一個附帶公開冷靜期的事件。

鏈上還是鏈下:Snapshot、gas,與免 gas 的投票

目前為止談的一切,都是具約束力的鏈上治理:投票與行動共享同一條交易軌跡,並由程式碼把結果執行出來。這份誠實同時也是它的成本——每一張投出的票都是一筆要付 gas 的交易,這會把小持有者排除在外,也讓一場激烈爭奪的投票變得昂貴。於是多數 DAO 在審議階段,會改用一個更便宜的層。

最主流的工具是 Snapshot,一個 鏈下 平台——在那裡,一張票只是儲存在鏈下的一則簽署訊息,並依某個過去區塊讀到的代幣餘額來加權。它免費、免 gas,所以任何人都能參與——但一場 Snapshot 投票什麼都不會執行。它是一場民調。要把結果化為行動,仍得由一個受信任的 多簽 團隊親手去送出那筆交易。許多團隊跑的正是這種混合做法:先在 Snapshot 上便宜地表態,最後才把那一步具約束力的動作放上鏈。

不過,具約束力的治理已經把差距縮小了。OpenZeppelin 的 Governor 支援 `castVoteBySig`,讓持有者把票在鏈下簽好、再交由一個轉發者送出,於是投票者不必付 gas,而計票卻仍然完全在鏈上、且會自我執行。誠實的總結是:鏈下 Snapshot 用可執行性換取了成本與包容性;鏈上 Governor 用成本換取了可信中立、會自我執行的結果——而基於簽章的投票,則在不放棄可執行性的前提下,把一部分包容性救了回來。

當「一幣一票」不夠用時:那些替代方案

儘管機械裝置一應俱全,代幣加權投票仍有一個它無法靠工程消除的頑固缺陷:金權政治。一枚代幣一票,意味著 權重等於財富,於是一條巨鯨的偏好,可以直接壓過一千名更小、人數更多的持有者——而選民冷感(往往只有百分之幾的供給會去投票)讓這份財富變得廉價好用。委託緩解了參與度,卻把權力更加集中。已經有幾種替代選票試圖做得更好;這裡先給出其中兩種誠實的概要,完整的機制設計處理,則留給本階的第四篇。

二次方投票在乎的強度變得有代價。每位投票者會得到一筆「聲音額度」,而在某議題上投下 v 票要花 v 的平方——於是影響力只隨資源的平方根成長,讓一個熱切的少數,恰恰在它真的更在乎時,壓過一個無所謂的多數。但陷阱很殘酷:整個結論都假設每個人一筆配額。一旦額度變成可交易的代幣,巨鯨只要把持倉拆到許多錢包——一種 女巫 攻擊——平方根就會塌回成單純的財富投票。二次方投票把它的安全性,外包給了一個加密世界至今仍沒有乾淨解答的「真人證明」層。

信念投票 改去攻打時間那條軸。這裡沒有快照、也沒有固定視窗;你把代幣質押在某提案背後停留得愈久,你的支持就累積愈多信念,而提案在累積信念越過某門檻的那一刻通過。這天然地抵抗閃電貸——只持有單一區塊的資本幾乎累積不了信念,因為信念需要時間,而那正是閃電貸租不到的東西——而且它表達的是持久的偏好,而非單一區塊的一次猛衝。誠實的代價是:它很慢、它的衰減與門檻參數調起來很麻煩,而且它終究仍是代幣加權的,所以它抑制的是巨鯨的速度,而非它的體量