Week 14 — Governance & DAO Security

“A protocol is only as secure as the slowest signer in its multisig and the fastest flash-loan-capable wallet that can outvote them. Governance is where ‘decentralization’ meets balance sheets: the bug class isn’t a missing require, it’s a missing assumption about who, with how much capital, can buy how much voting power, over what window, before a timelock catches the community’s attention. Audit the proposal flow like you audit a function — every privileged call is a state mutation under adversarial input.”

Tags: web3-security governance dao timelock flash-loan multisig vetoken snapshot Learner: Past Tuan-08-DeFi-Security-AMM-Lending-Vault, Tuan-12-Wallet-AA-Key-Management, Tuan-13-Frontend-dApp-Infrastructure — ready to reason about who actually controls a protocol Time: 7 days (4–6h/day) Related: Tuan-04-Security-Foundations-CEI-AC · Tuan-15-Audit-Methodology-Tooling · Case-Beanstalk-Governance-2022 · Case-Tornado-Cash-Governance-2023


1. Context & Why

1.1 The auditor’s reframe of “decentralization”

Marketing departments call a protocol “decentralized” when the token is widely held and there’s a Snapshot space. The auditor’s reframe is sharper:

A protocol is decentralized only along the axis along which an attacker cannot acquire enough of a privileged role — for cheap enough, fast enough — to alter the protocol’s behavior in ways the holders would object to.

That definition makes the audit question concrete: for every action that can change protocol behavior (upgrade, parameter change, treasury spend, oracle swap, role grant), what does it cost an attacker — in capital, in time, in social cost — to do it?

Concrete examples that flip what “decentralized” means:

Protocol claimReality the auditor must check
”Governed by token holders”Is voting power live-balance or checkpointed? Live balance = $1B flash loan ≈ governance takeover.
”Timelock controls upgrades”What’s the delay? Who holds PROPOSER_ROLE, EXECUTOR_ROLE, CANCELLER_ROLE, DEFAULT_ADMIN_ROLE? Is address(0) the executor (open execution) or is execution gated?
”Multisig is just an emergency backstop”Trace every privileged function. Some “emergency” multisigs can rotate the governor, the timelock, the oracle, the upgrade authority — i.e., the multisig is the protocol.
”Off-chain Snapshot + on-chain execution via SafeSnap”Whose Safe? What’s the quorum? Who can trigger execution? Is the Reality.eth bond enough to deter spam?
”Renounced ownership”Renouncing Ownable is meaningless if the contract has another onlyRole(ADMIN) somewhere, a proxy admin, or a _setOracle() callable by a different role.

Every governance audit boils down to: enumerate every state-mutating privileged path, then for each one answer (a) who can call it, (b) under what delay, (c) under what review, (d) what’s the worst thing it can do, (e) what’s the cost to compel that action. Everything else is decoration.

1.2 Why governance bugs are catastrophic

A reentrancy bug drains a vault. A governance bug rewrites the protocol. The blast radius is total because once the attacker controls governance, they control:

  • The implementation behind the proxy (rewrite logic).
  • The timelock (shorten or remove the delay).
  • The oracle (set their own price feed and drain the lending market).
  • The treasury (transfer everything to themselves).
  • The token supply (mint to themselves; e.g., the Beanstalk attacker minted via the diamond’s mint after taking control).
  • The pause guardian (turn off circuit breakers so users can’t react).

Beanstalk lost $182M in April 2022 from a single malicious proposal executed via flash-loaned voting power. The cost to the attacker was Aave/Uniswap/Sushi flash-loan fees, plus 1 day of patience, plus a deliberately innocuous-looking proposal posted earlier. The cost to Beanstalk was 100% of TVL.

Compound came within 51% / 49% of losing $24M of treasury COMP to a hostile delegate in Proposal 289 (July 2024) — the attacker stood down only after a side deal. Tornado Cash’s governance was outright captured in May 2023 via a malicious proposal that hid a selfdestruct behind innocuous-looking metadata. None of these were “smart contract bugs” in the reentrancy sense — they were governance design failures that an auditor could and should have flagged pre-launch.

1.3 What you’ll be able to do by Friday

  • Diagram the OpenZeppelin Governor + Timelock + ERC20Votes stack and identify every role, every delay, every threshold.
  • Decode a real governance proposal’s calldata, target-by-target, and write a one-paragraph plain-English summary of what it does.
  • Distinguish flash-loan-vulnerable governance from snapshot-protected governance by reading the voting strategy and proposal-creation hook.
  • Identify timelock bypass paths (delegatecall from a module, ungated executor role, missing role rotation).
  • Write a “Centralization Risks & Governance” appendix for a small protocol that catalogs every admin / timelock-managed role with a severity tag per role.

1.4 Primary references

SourceURLStatus
OpenZeppelin Governor APIhttps://docs.openzeppelin.com/contracts/api/governanceCurrent (v5.x as of 2026-05 [verify])
OpenZeppelin TimelockControllerhttps://docs.openzeppelin.com/contracts/api/governance#TimelockControllerCurrent
OpenZeppelin Governor tutorialhttps://docs.openzeppelin.com/contracts/governanceCurrent
Compound Governor Bravo sourcehttps://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/GovernorBravoDelegate.solProduction; canonical legacy reference
Compound Governance docshttps://compound.finance/docs/governancePartial — still accurate on Bravo, but Compound v3 introduced new admin patterns
Curve veCRV docshttps://docs.curve.finance/curve_dao/voting-escrow/voting-escrow/Current
Snapshot docshttps://docs.snapshot.org/Current
SafeSnap / Reality.eth integrationhttps://docs.snapshot.org/user-guides/plugins/safesnap-realityCurrent
Tally docshttps://docs.tally.xyz/Current
a16z — DAO governance attacks and how to avoid themhttps://a16zcrypto.com/posts/article/dao-governance-attacks-and-how-to-avoid-them/Current
a16z podcast — Governance Attack!?https://a16zcrypto.com/posts/podcast/governance-attacks-dao-political-systems-compound-treasury/Current
Vitalik — Moving Beyond Coin Voting Governance (2021)https://vitalik.eth.limo/general/2021/08/16/voting3.htmlConceptually current
Vitalik — Blockchain Voting Is Overrated… (2021)https://vitalik.eth.limo/general/2021/05/25/voting2.htmlConceptually current
Optimism — The Future of Optimism Governancehttps://www.optimism.io/blog/the-future-of-optimism-governanceCurrent
Immunefi — Beanstalk hack analysishttps://medium.com/immunefi/hack-analysis-beanstalk-governance-attack-april-2022-f42788fc821eHistorical; canonical post-mortem
Composable Security — Tornado Cash governance attackhttps://composable-security.com/blog/understanding-the-tornado-cash-governance-attack/Current
Halborn — Beanstalk hack explainedhttps://halborn.com/explained-the-beanstalk-hack-april-2022/Historical
DeSpread — Compound Proposal 289 recaphttps://research.despread.io/compound-finance-governance-attack/Current
Cyfrin — Governance Attack glossaryhttps://www.cyfrin.io/glossary/governance-attackCurrent
1Hive — Conviction votinghttps://github.com/1Hive/conviction-voting-cadcadCurrent; reference implementation
Gitcoin — Quadratic voting / fundinghttps://gitcoin.co/mechanisms/quadratic-votingCurrent

2. Governance Models — a Taxonomy

Before reading any specific Governor contract, internalize the seven models in production today. Each has a distinct attack surface and a distinct auditor checklist.

2.1 Token voting (1 token = 1 vote)

The dominant model. Compound (COMP), Uniswap (UNI), Aave (AAVE), most OZ-Governor deployments. Voting power = balance of governance token at a snapshot block.

Pros: Simple, transparent, low-friction, fits ERC-20 mental model.

Cons (the auditor’s catalog):

  • Plutocracy: wealth = power. Whales dominate; ordinary holders rationally don’t vote.
  • Buyable: tokens are liquid. A bad actor can market-buy or flash-loan voting power.
  • Apathy: most holders never vote; quorum is hit by a small active minority. Compound proposal 289 was decided by a 51/49 vote — most COMP didn’t participate.
  • Delegation centralization: most users delegate, often to the same few “vote market” delegates. Tally consistently shows top-10 delegates holding the decisive bloc in major DAOs.

Auditor angle: even if the math is “1 token 1 vote”, check whether the snapshot mechanism uses checkpointed historical balances (ERC20Votes) — without this, flash loans are catastrophic. See §6.

2.2 Vote-escrow (veToken)

Pioneered by Curve (veCRV), copied by Balancer (veBAL), Frax (veFXS), and many others. Users lock the governance token for a chosen duration (Curve: 1 week to 4 years). Voting power is time-weighted:

veCRV = CRV × (lockTimeRemaining / maxLockTime)

So locking 1 CRV for 4 years yields 1 veCRV; locking 1 CRV for 1 year yields 0.25 veCRV. Voting power decays linearly to zero as the unlock date approaches. veCRV is non-transferable.

Pros:

  • Decouples speculation from governance — to vote you must commit.
  • Flash loans are useless: you can’t lock and unlock in one transaction; minimum lock is 1 week.
  • Aligns governance with long-term holders.

Cons (the audit lens):

  • Liquid wrappers / meta-governance: Convex (cvxCRV) accepts CRV → locks it → issues a liquid liquid token. The voting power is now controlled by Convex governance (vlCVX), not the original holders. By aggregating >50% of veCRV, Convex effectively controls Curve gauges.
  • Bribe markets: Votium, Hidden Hand, Warden — paid services where protocols bribe vlCVX/veBAL holders to vote a particular way. Voting is now a yield-bearing market.
  • Whale lock-in: a whale who locks for 4 years has stable control over emissions for years.

Auditor angle: when auditing a veToken protocol, check the voting_escrow.vy (or equivalent) contract for: (a) lock-time bounds, (b) decay formula correctness, (c) re-lock / extension semantics, (d) admin functions that can short-circuit lockup (e.g., emergency unlock).

2.3 Conviction voting (1Hive Gardens, Commons Stack)

Token holders stake on proposals continuously. Their “conviction” — voting power on a proposal — accumulates over time according to an exponential curve:

conviction(t) = stake × (1 - α^t)   where α ∈ (0,1)

A proposal passes once its conviction exceeds a threshold (typically a function of total token supply). Decisions are not discrete vote events; they are continuous funding flows.

Pros:

  • Resistant to last-minute attacks: conviction takes weeks to build, so flash-loaned or recently-purchased tokens have near-zero immediate impact.
  • Captures preference intensity: holders re-allocate stake as priorities shift.
  • No “voting day” — fits ongoing grant funding (1Hive Gardens, Commons Stack).

Cons:

  • Hard to use for time-sensitive decisions (emergency pauses, urgent upgrades).
  • Token-weighted, so still plutocratic in principle.
  • Complex UX; few users beyond 1Hive’s community have adopted it.

Auditor angle: rare in DeFi audits, but if you encounter it, the bug surface is the conviction formula and its parameters — α, decay over time, threshold curve. Verify against the protocol’s published parameters and the reference implementation (conviction-voting-cadcad).

2.4 Quadratic voting / Quadratic funding (Gitcoin)

Voters have a fixed budget of “voice credits”. Cost of N votes for a single option = N². So 1 vote costs 1 credit, 2 votes cost 4 credits, 10 votes cost 100 credits. This penalizes concentrated preferences and rewards breadth.

Quadratic funding (Gitcoin Grants) is the matching variant: the matching pool subsidizes projects according to:

match = (Σ √contribution_i)²

Many small contributors beat one whale. Mathematically elegant; Sybil-vulnerable in the extreme.

Pros:

  • Best mathematical anti-plutocracy mechanism.
  • Surfaces preferences of small holders.

Cons (the audit-killer):

  • Sybil-fragile: a single user splitting into 100 wallets gets quadratic leverage. Identity is now the security primitive — but every onchain identity system (Gitcoin Passport, Worldcoin, BrightID) has tradeoffs (centralization, biometric trust, ZK-proof complexity).
  • Early Gitcoin rounds were riddled with Sybil collusion; pivoted to Gitcoin Passport scoring.

Auditor angle: any quadratic voting deployment must be audited for its identity/Sybil-resistance layer. The on-chain math is the easy part; the off-chain proof-of-personhood pipeline is the soft underbelly.

2.5 Optimistic governance

Proposals are assumed valid unless challenged. Common in Optimism ecosystem forks: a proposal sits in a challenge window; if nobody challenges within T, it executes. Challenge requires posting a bond; successful challenge slashes the proposer.

Pros: cheap on-chain footprint; only contested decisions require active voting.

Cons:

  • Requires vigilant watchers during the challenge window. If everyone is asleep, malicious proposals slip through.
  • Bond design is everything — too low and frivolous challenges flood; too high and only whales can challenge.

Auditor angle: who watches the window? Are there incentivized challenger bots? What happens on a public holiday? Bond size relative to economic value at risk?

2.6 Off-chain voting (Snapshot) + on-chain execution (SafeSnap, Reality.eth)

Voting happens off-chain on Snapshot — gas-free, no on-chain footprint. Execution requires a binding bridge: SafeSnap is the dominant pattern.

The flow:

  1. Proposal posted on Snapshot with a payload (target, value, calldata).
  2. Token-holders vote off-chain (signed messages, free).
  3. If proposal passes, a Reality.eth question is opened: “Did Snapshot proposal X pass with payload Y?”
  4. After a bond + cooldown (24h typical), if Reality.eth resolves Yes, the Gnosis Safe executes the payload.
  5. Anyone can challenge the Reality.eth answer by posting a higher bond; final answer is the highest-bonded one after the dispute window.

Pros: gas-free voting, much higher participation, low DAO operating cost.

Cons (the auditor’s red list):

  • Reality.eth oracle dispute window is the security backstop. If nobody disputes a fraudulent Yes answer, the Safe executes a malicious payload. Bond sizing matters.
  • Snapshot signature spoofing: Snapshot relies on off-chain signatures; if a delegate’s key is phished, attacker can vote with all delegated power.
  • Off-chain UX confusion: voters can be tricked by malicious proposal descriptions while the payload does something different. Reality.eth resolvers are supposed to verify this — in practice, lazy resolvers rubber-stamp.

Auditor angle: audit the Reality.eth question template; audit the bond amount; audit the Safe’s signer list and threshold; audit the module’s executeProposalWithIndex flow; verify there’s no way to swap the Snapshot space mid-flow.

2.7 Hybrid: multisig with elected signers / Security Council

The default model for most production protocols. A multisig (typically a Gnosis Safe) holds upgrade authority, with N-of-M signers elected via governance vote or appointed at launch. Often paired with a token-voting Governor: routine parameter changes via Governor, emergency changes via multisig.

The Optimism Security Council model: a multisig of trusted ecosystem entities (foundations, audit firms) holds emergency upgrade authority. Their actions are visible on-chain; if the council acts contrary to the Token House / Citizens’ House, social pressure forces resignation.

Pros: fast in emergencies; doesn’t require flash-loan-resistant Governor design.

Cons:

  • Trust the signers. Period.
  • Signer compromise = full takeover (Radiant 2024: owner key compromise via malware drained $50M+ [verify]).
  • Signer collusion is undetectable until used.

Auditor angle: see §10. Multisig audit is its own discipline.

2.8 Summary matrix

ModelFlash-loan resistant?Plutocracy-resistant?Time to decideProduction examples
Live-balance token voteNoNoHours(vulnerable; mostly historical)
Snapshot-based token vote (ERC20Votes)Yes (if voting delay ≥ 1 block)NoDaysCompound, Uniswap, Aave
veTokenYes (1-week min lock)Partial (long lockers win)WeeksCurve, Balancer, Frax
ConvictionYes (slow accumulation)NoDays–weeks1Hive Gardens
QuadraticNo (Sybil-dependent)Yes (if Sybil-resistant)DaysGitcoin Grants
OptimisticYes (challenge window)DependsHours–days (per window)OP-stack governance variants
Snapshot + SafeSnapYesNoDaysMany mid-sized DAOs
Multisig onlyYesTrust-based, not plutocraticMinutesMost prod protocols (admin)

The auditor reads the protocol’s governance architecture against this table on Day 1.


3. OpenZeppelin Governor — Architecture, Settings, and Every Knob

OZ Governor is the most-deployed governance contract framework. Read its modules, settings, and proposal flow until they are reflexes.

3.1 Architecture overview

flowchart TD
  User[Proposer / Voter] --> Gov[Governor<br/>governor.sol]
  Gov --> Votes[Voting power source<br/>IVotes interface]
  Gov --> Count[Counting module<br/>For/Against/Abstain/Fractional]
  Gov --> Quorum[Quorum module<br/>Fraction of supply]
  Gov --> TL[TimelockController<br/>scheduled execution]
  TL --> Targets[Target contracts<br/>protocol implementation]
  Votes --> Token[ERC20Votes / ERC721Votes<br/>checkpointed balances]
  
  style Gov fill:#fff2cc
  style TL fill:#ffcccc
  style Votes fill:#cce5ff

The Governor is abstract — you compose a concrete deployment from modules.

3.2 The modules (composition pattern)

A real deployment looks like:

contract MyGovernor is
    Governor,                            // core
    GovernorSettings,                    // voting delay/period/threshold
    GovernorCountingSimple,              // For/Against/Abstain
    GovernorVotes,                       // reads voting power from IVotes
    GovernorVotesQuorumFraction,         // quorum = % of total supply
    GovernorTimelockControl              // execution via TimelockController
{
    constructor(IVotes _token, TimelockController _timelock)
        Governor("MyGovernor")
        GovernorSettings(
            1 days,        // votingDelay
            1 weeks,       // votingPeriod
            100_000e18     // proposalThreshold (tokens)
        )
        GovernorVotes(_token)
        GovernorVotesQuorumFraction(4)   // 4% quorum
        GovernorTimelockControl(_timelock)
    {}
    // Required overrides for diamond-inheritance resolution
    function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) {
        return super.votingDelay();
    }
    // ... similar overrides for votingPeriod, proposalThreshold, etc.
}

Each module is a distinct audit surface:

ModuleWhat it controlsAudit checklist
Core GovernorProposal lifecycleAre propose, castVote, queue, execute correctly state-gated?
GovernorSettingsvotingDelay, votingPeriod, proposalThresholdAre values sane? Who can change them? (Only via governance itself.)
GovernorCountingSimpleFor / Against / Abstain”Bravo” semantics; quorum counts Abstain (deliberate).
GovernorCountingFractionalSplit votes across multiple optionsEnables delegated-pool voting; audit weight sum invariant.
GovernorCountingOverridableDelegators can override delegates’ votesAudit override window timing.
GovernorVotesRead voting power from IVotesVerify IVotes.getPastVotes(account, timepoint) is the call used — historical, not current.
GovernorVotesQuorumFractionQuorum as % of past total supplyAudit denominator is IVotes.getPastTotalSupply() (historical).
GovernorVotesSuperQuorumFractionSuper-majority quorum optionNewer; verify version (OZ ≥ v5.x [verify]).
GovernorTimelockControlExecution via TimelockControllerAudit _execute flows through timelock; audit relay function.
GovernorTimelockAccessExecution via AccessManagerAudit AccessManager role mapping.
GovernorTimelockCompoundCompound-style Timelock integrationLegacy compatibility.

3.3 The proposal lifecycle

stateDiagram-v2
    [*] --> Pending: propose()
    Pending --> Active: after votingDelay
    Active --> Succeeded: votingPeriod ends, quorum met, For > Against
    Active --> Defeated: votingPeriod ends, quorum NOT met OR For <= Against
    Active --> Canceled: cancel()
    Succeeded --> Queued: queue() [with timelock]
    Queued --> Executed: execute() after timelock delay
    Queued --> Expired: grace period elapsed
    Succeeded --> Executed: execute() [no timelock]
    Defeated --> [*]
    Canceled --> [*]
    Executed --> [*]
    Expired --> [*]

Six states an auditor must reason about. The most-missed: Queued → Expired. The timelock’s grace period means a queued proposal that’s not executed in time becomes void — useful for cancelling stale proposals socially, but if your protocol depends on a particular proposal being executed, the grace period is a footgun.

3.4 The four critical settings

Memorize these and their typical values:

SettingWhatTypical valuesRisk if too lowRisk if too high
votingDelayBlocks/seconds between propose() and vote start1–2 days (Compound: 13140 blocks ≈ 2 days; OZ: configurable)Voters can’t react; flash-loan risk increasesSlows legitimate governance; emergency response is impossible
votingPeriodDuration voters can cast votes3–14 days (Compound Bravo: 17280 blocks ≈ 3 days; many DAOs: 1 week)Insufficient participation windowStale proposals; coordination costs
proposalThresholdMinimum tokens to create a proposal0.25–1% of supply (Compound Bravo: 25,000 COMP)SpamOnly whales can propose
quorumMinimum votes needed for validity2–10% of supply (Compound Bravo: 400,000 COMP = 4%)Low-participation attacks (Proposal 289 shape)Permanent gridlock

Compound Bravo bounds (from GovernorBravoDelegate.sol):

  • MIN_PROPOSAL_THRESHOLD = 1,000 COMP
  • MAX_PROPOSAL_THRESHOLD = 100,000 COMP
  • MIN_VOTING_PERIOD = 5,760 blocks (~24h)
  • MAX_VOTING_PERIOD = 80,640 blocks (~2 weeks)
  • MIN_VOTING_DELAY = 1 block
  • MAX_VOTING_DELAY = 40,320 blocks (~1 week)

These bounds are enforced in the contract — admin attempts to set outside them revert. Many forks inherit these bounds; verify by reading the deployed bytecode/source.

3.5 ERC20Votes — the underrated workhorse

The flash-loan defense lives here, not in the Governor.

contract MyToken is ERC20, ERC20Permit, ERC20Votes {
    constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {}
    // OZ v5 overrides required
    function _update(address from, address to, uint256 value)
        internal override(ERC20, ERC20Votes)
    {
        super._update(from, to, value);
    }
}

What ERC20Votes does:

  • On every transfer, write a checkpoint (blockNumber, balance) for both sender and receiver’s delegate.
  • getPastVotes(account, blockNumber) reads from these checkpoints — returns the balance the account’s delegate held at that historical block.
  • delegate(addr) assigns voting power. Without an explicit delegate() call, your tokens have zero voting power (a UX surprise that has decided real proposals).

Why this prevents flash-loan attacks:

t0:   blockNumber = 100, attacker holds 0 tokens
t1:   propose() called at block 100. Governor records snapshot block = 101 (after votingDelay = 1 block).
t2:   blockNumber = 101, Governor reads attacker's voting power at block 101.
t3:   blockNumber = 200, voting opens. Attacker takes flash loan of 1B tokens.
t4:   castVote() called. Governor calls getPastVotes(attacker, 101) → returns 0.
t5:   Attacker's vote weight is 0 despite holding 1B tokens at vote time.

The snapshot is taken at proposal-creation time + voting delay, before the attacker can flash-borrow. As long as voting delay ≥ 1 block and the attacker can’t predict the proposal far in advance, flash loans are inert.

The Beanstalk bug was exactly the absence of this: Beanstalk’s Stalk token’s voting power was read from the live balance at execution time, not from a historical checkpoint. Attacker → flash loan → deposit → vote → execute → withdraw → repay loan, all in one transaction.

Auditor angle: open the IVotes implementation. If getVotes() or any vote-weight function returns current balance instead of a historical checkpoint, that’s a critical-severity finding. The Governor cannot fix this; the token must be fixed.


4. Compound Governor Bravo — Historical and Still Live

Most DeFi-era DAOs (Compound, Uniswap V3 governance, many forks) run Governor Bravo, the upgraded version of Compound’s Alpha. It is the reference Solidity implementation that OZ later generalized and modularized.

4.1 Architecture

GovernorBravoDelegator (proxy, no logic)
   ↓ delegatecall
GovernorBravoDelegate (logic)
   ↓ calls
Comp (ERC20 + voting checkpoints, custom — not OZ)
   ↓ for execution
Timelock (Compound's own, predates OZ TimelockController)
   ↓ executes
Comptroller / cToken / oracle / etc.

Bravo is a proxy with upgradeable logic. The upgrade authority is Bravo itself (governance proposes an upgrade to its own delegate). The Comp token has its own checkpoint system (getPriorVotes(address, blockNumber)) — semantically equivalent to ERC20Votes.getPastVotes.

4.2 Differences from OZ Governor

AspectGovernor BravoOpenZeppelin Governor
ArchitectureMonolithic Solidity contract behind a proxyModular composition (multiple mixins)
Voting power sourceHardcoded to Comp.getPriorVotesPluggable IVotes
CountingFor/Against/Abstain (added in Bravo vs Alpha’s For/Against)Pluggable counting module
QuorumFixed quorum (400k COMP)Pluggable (fraction or fixed)
TimelockCompound’s Timelock contract (not OZ TimelockController)Pluggable
Bound enforcementHardcoded MIN/MAX in contractConfigurable but governance can change
Upgrade pathSelf-upgradeable via proposalTypically immutable (or behind separate proxy)

4.3 Bravo’s specific knobs to remember

// From GovernorBravoDelegate.sol (Compound)
uint public constant MIN_PROPOSAL_THRESHOLD = 1_000e18;
uint public constant MAX_PROPOSAL_THRESHOLD = 100_000e18;
uint public constant MIN_VOTING_PERIOD = 5_760;     // ~24 hours in blocks
uint public constant MAX_VOTING_PERIOD = 80_640;    // ~2 weeks
uint public constant MIN_VOTING_DELAY = 1;
uint public constant MAX_VOTING_DELAY = 40_320;     // ~1 week
uint public constant quorumVotes = 400_000e18;      // 4% of total supply
uint public constant proposalMaxOperations = 10;    // batched calls per proposal

proposalMaxOperations = 10 is interesting — proposals are batched calls (multiple (target, value, signature, calldata) tuples). Maximum 10 actions per proposal. An attacker who needs more must split into multiple proposals.

4.4 The state machine in Bravo

Same shape as OZ:

Pending → Active → (Defeated | Succeeded | Canceled)
Succeeded → Queued → (Executed | Expired)

Reachable via state(proposalId) returning a ProposalState enum.

4.5 Why Bravo still matters in 2026

Roughly half of mid-cap DeFi DAOs are running Bravo forks: Uniswap V3, Compound III, Convex, Yearn, Frax. When auditing one of these, you can’t just refer to OZ docs — you must read the Bravo code in your audit-target’s repo. Common diff areas:

  • Custom quorum logic (e.g., percentage of totalSupply rather than fixed).
  • Custom voting periods.
  • Adapted Comp token (some forks use ERC20Votes instead of Compound’s Comp).
  • Timelock variants (some forks point Bravo at OZ’s TimelockController instead of Compound’s Timelock).

The audit value is: every change relative to the canonical Bravo source is a place where a bug was introduced and must be reviewed.


5. TimelockController — the Defense That Decides Whether a Bug Becomes a Loss

The timelock is the most important contract in a DAO from a security standpoint. It is the delay between a malicious proposal passing and the malicious action executing. That window is when watchers see the proposal, sound the alarm, and either rally a counter-vote, exit the protocol, or guardian-pause.

5.1 OZ TimelockController architecture

flowchart LR
  Gov[Governor] -- schedule --> TL[TimelockController]
  Multisig[Emergency Multisig] -- schedule --> TL
  TL -- delay >= minDelay --> Ready[Ready state]
  Ready -- execute --> Target[Target contracts]
  Cancel[Canceller] -- cancel --> TL
  
  style TL fill:#ffcccc

The TimelockController has four roles:

RoleConstantWhatTypical assignee
PROPOSER_ROLEkeccak256("PROPOSER_ROLE")Schedule operationsGovernor contract
EXECUTOR_ROLEkeccak256("EXECUTOR_ROLE")Execute ready operationsOften address(0) = open execution
CANCELLER_ROLEkeccak256("CANCELLER_ROLE")Cancel scheduled operationsEmergency multisig / Governor
DEFAULT_ADMIN_ROLE0x00Grant/revoke other rolesShould be the timelock itself post-setup

5.2 The operation lifecycle

Unset → schedule() → Pending → (wait ≥ minDelay) → Ready → execute() → Done
                          ↘ cancel() → Unset

schedule(target, value, data, predecessor, salt, delay) records:

  • Operation id = keccak256(abi.encode(target, value, data, predecessor, salt)).
  • Timestamp = block.timestamp + delay (or getMinDelay() if delay < min).

execute(target, value, data, predecessor, salt) recomputes the id and checks block.timestamp >= timestamps[id].

scheduleBatch / executeBatch operate atomically over arrays of operations.

5.3 Common configurations and what they mean

DelayUse caseAuditor’s note
0 (no delay)Emergency multisig “live wire”Effectively bypasses timelock; treat as critical-trust role
6hFrequent parameter changes (perp funding, fee adjustments)Suspiciously short for upgrade authority
2 daysCompound’s default for non-emergencyCommon DeFi standard; sufficient for community alarm
3 daysAave, Uniswap (varies)More conservative
7 daysLido, certain Compound v3 pathsHigh-value protocols; sufficient watcher window
14 days+MakerDAO Pause Proxy (GSM)Maximum-conservative; only top-tier protocols
30 daysSome L2 admin upgrades (cf. Optimism’s evolving model)Reasonable for L1 contracts holding L2 funds

Rule of thumb: the timelock delay should exceed the time required for: (a) the community to read the proposal calldata, (b) reasonable auditors to identify malicious intent, (c) users to exit if they choose. For DeFi protocols with significant TVL, anything under 48h is a finding.

5.4 Misconfigurations — the audit catalog

5.4.1 Wrong admin

Common bug: deploy timelock, give Governor PROPOSER_ROLE, but forget to renounce DEFAULT_ADMIN_ROLE from the deployer EOA. The deployer can now grant themselves any role, including PROPOSER_ROLE and EXECUTOR_ROLE, and schedule and execute arbitrary operations with arbitrary delay (since admin can set minDelay to 0 via the timelock itself).

Audit signal:

# Read AccessControl roles
cast call $TIMELOCK "hasRole(bytes32,address)(bool)" 0x00 $DEPLOYER
# Should be false post-deployment

5.4.2 Open executor

EXECUTOR_ROLE granted to address(0) means anyone can execute a ready operation. This is intentional — it democratizes execution after the delay, since the proposer can no longer alter the operation post-schedule. However, this only works if scheduling is gated (PROPOSER_ROLE is held only by trusted entity, typically Governor).

If both PROPOSER_ROLE and EXECUTOR_ROLE are open (address(0)), the timelock is effectively a public delay queue with no governance — anyone can schedule anything and wait it out.

5.4.3 No canceller

If CANCELLER_ROLE is unset, the only way to cancel a scheduled malicious operation is for governance to pass a counter-proposal — which takes longer than the timelock delay itself. The cancel is then useless. Best practice: grant a small emergency multisig the canceller role.

5.4.4 Timelock with admin = deployer = EOA = no signature requirements

Found in many tutorials and copy-paste production. The deployer becomes the implicit god-mode admin. Always trace DEFAULT_ADMIN_ROLE to either (a) the timelock itself (renounced from deployer) or (b) a properly-configured multisig.

5.5 Timelock bypass paths

The most dangerous audit findings are paths that execute privileged actions without going through the timelock. Catalog:

5.5.1 Delegatecall facets in a Diamond

Diamond proxies (EIP-2535) route calls to facets via selectors. If governance can upgrade facets (diamondCut), and the upgrade itself doesn’t go through the timelock, the timelock is bypassable: vote → diamondCut → new facet executes immediately. Mitigation: diamondCut must itself be timelock-gated.

5.5.2 Module / plugin systems

Safe modules, Aragon apps, Compound’s _setPriceOracle, MakerDAO’s MIP authorities — any address that has direct privileged access bypassing the timelock is a bypass path. List every such address.

5.5.3 Emergency / pause guardian with too much power

A “pause” guardian should be able to stop activity, not change activity. If the guardian can swap oracles, upgrade implementations, drain treasury — that’s not a pause, that’s a parallel admin. Audit finding: scope-creep of emergency roles.

5.5.4 Direct admin functions on dependencies

A protocol’s Oracle.setPriceFeed() may be callable by the timelock — but if the underlying Chainlink aggregator is administered by Chainlink Labs’s multisig, that’s a dependency-level admin. The protocol’s timelock cannot protect what it does not control.

5.5.5 Same-block proposal + execute via emergency function

The Beanstalk bug: emergencyCommit() allowed a proposal to execute after 1 day if it reached 2/3 supermajority. The “1 day” was not a real timelock because:

  1. The attacker had pre-submitted the (innocuous-looking) proposal earlier.
  2. After 1 day of eligibility, the attacker triggered execution in the same transaction as the flash-loaned vote.
  3. The 1-day delay was between proposal submission and voting eligibility, not between vote completion and execution.

So the operative delay between malicious vote and malicious execution was zero blocks. Timelock semantics ≠ named timing constants. Read the code paths.

5.6 The “set minDelay back to 0” attack

Some implementations expose updateDelay(uint256 newDelay) callable by the timelock itself. Sounds safe (it requires the delay to update via a scheduled call). But:

  1. Attacker passes a proposal: “Update minDelay to 0 seconds.”
  2. Proposal goes through the existing (e.g., 2-day) delay.
  3. Now any future proposal can be executed instantly.
  4. Attacker passes a second proposal to drain treasury / rewrite implementation.
  5. Drain executes immediately.

The first proposal was suspicious; the second was the kill. Audit checklist: is changing minDelay itself a high-stakes operation that an alert community can catch? Are there hard floor bounds (e.g., minDelay can never be set below 24h)?


6. Delegation, Snapshot Timing, and Flash-Loan Governance

6.1 The delegation flow

In ERC20Votes:

function delegate(address delegatee) public {
    _delegate(_msgSender(), delegatee);
}

This sets the caller’s voting power to be exercised by delegatee. Until called, the caller’s tokens have zero voting weight. Most users delegate to themselves: token.delegate(msg.sender). Many do not — and lose their voice.

Implications for the audit:

  • Top-10 delegates often hold the majority of effective voting power, even though token distribution is wider. Tally’s data on major DAOs consistently shows this.
  • A delegate’s key compromise = compromise of every token-holder who delegated to them. Delegation is a trust assumption.
  • Delegating is a transaction — costs gas. Users who hold via CEXs or never claim airdrops typically never delegate, which entrenches active delegates’ power further.

6.2 Snapshot timing — the flash-loan defense

The snapshot block determines whose voting power counts. The Governor calls:

// In OZ Governor._castVote
uint256 weight = _getVotes(account, proposalSnapshot(proposalId), params);

proposalSnapshot(proposalId) returns the block at which voting power was snapshotted — typically the proposal-creation block + votingDelay. The voting delay’s job is precisely to separate the snapshot from any block at which the attacker could anticipate the proposal.

Audit checklist for snapshot timing:

  • What is votingDelay? At minimum 1 block. Recommended ≥ 1 day for major DAOs.
  • Does _getVotes use a historical lookup (getPastVotes)? If it reads getVotes (current), that’s the Beanstalk bug.
  • Can the attacker submit a proposal with a back-dated snapshot? (Usually no, because snapshot is creation-block + delay, but check custom forks.)
  • Are checkpoint writes triggered on every transfer? (ERC20Votes._update ensures this; custom token wrappers may not.)

6.3 Flash-loan governance attack — the canonical reproduction

The attack flow when voting power is read from live balance (not checkpointed):

sequenceDiagram
    participant A as Attacker
    participant L as Aave (Flash Loan)
    participant G as Vulnerable Governor
    participant V as Victim Protocol

    Note over A: Step 0: pre-submit innocuous-looking proposal
    A->>G: propose(payload = drain to attacker)
    Note over G: Proposal sits in queue
    Note over A: ... time passes; voting opens ...
    A->>L: flashLoan(1B governance token)
    L->>A: 1B tokens
    A->>A: delegate(self)
    A->>G: castVote(proposalId, FOR, weight=1B)
    Note over G: Vote weight read at live balance = 1B
    A->>G: execute(proposalId)
    G->>V: drainTreasury()
    V->>A: treasury funds
    A->>L: repay flash loan + fee

In Beanstalk’s case, the emergencyCommit path required no separate execution call once 2/3 was reached — the vote was the execution trigger.

Mitigations:

  1. Snapshot-based voting power (ERC20Votes + Governor with getPastVotes) — attacker’s flash-loaned balance is irrelevant.
  2. Voting delay ≥ N blocks between propose and voting start — attacker cannot back-date the snapshot.
  3. Voting period ≥ T days — even if attacker controls majority of effective votes, the period allows defensive coordination.
  4. Timelock ≥ D days between vote success and execution — community window to call out malicious proposals.
  5. veToken locking — making voting power non-transferable defangs flash-loan attacks entirely (Curve’s model).
  6. Quorum on participation, not just supply — if 5% of supply is required and your normal participation is 3%, governance is rugged but not in the bad way. If 4% is required and an attacker can supply 5% of total tokens (flash loan), proposal passes.

6.4 Flash-delegation: the snapshot manipulation variant

Subtle attack: even if ERC20Votes checkpoints transfers, delegations also write checkpoints. An attacker with no tokens can:

  1. Convince 100 users to delegate to a contract address (e.g., via a phishing site styled as “vote rewards portal”).
  2. The contract address now has voting power = sum of delegated balances.
  3. Attacker controls the contract → controls the votes.

This isn’t a flash loan, but it’s a snapshot manipulation of similar flavor. The defense is social (don’t delegate to unknown addresses) and UX (wallets warning on delegate calls), not protocol-level.

There’s also a more technical variant: flash delegation in protocols that allow delegation power to be read from the current block (rather than checkpointed). Same shape as the live-balance bug but on the delegation map. Audit: verify delegation lookups also use getPastVotes semantics.


7. Curve veCRV and Meta-Governance (Curve Wars)

7.1 The veCRV system

The VotingEscrow contract (Vyper) holds locked CRV and tracks per-user voting weight:

veCRV_balance(user, t) = locked_amount × (lock_end - t) / max_lock_time

where max_lock_time = 4 years (in seconds: 4 × 365 × 86400, with some week-alignment rounding). Voting power decays linearly to zero as t approaches lock_end.

Key invariants for an auditor:

  • lock_amount ≥ 0, never decreasing without an explicit withdraw (and withdraw is only possible after lock_end).
  • lock_end is week-aligned and at most now + 4 years.
  • Re-locking extends lock_end; new amount = old + added; voting weight recomputed.
  • veCRV is non-transferable — there’s no transfer() on the VotingEscrow.
  • Smart contracts cannot lock unless explicitly whitelisted by SmartWalletWhitelist — a defensive measure against speculative liquid wrappers (though Convex was whitelisted; see meta-governance below).

7.2 Gauges and emissions

Curve’s GaugeController weights pools’ CRV emissions according to veCRV votes. Each week, veCRV holders allocate their voting weight across gauges; the next week’s emissions follow those weights.

This creates the economic gravity that drove Curve Wars: protocols want their pool to receive emissions (so it has deep liquidity), so they want to control veCRV votes.

7.3 Convex — meta-governance and liquidity wrappers

Convex (cvxCRV) accepts CRV → locks it in veCRV → mints liquid cvxCRV to the depositor. Convex itself holds the veCRV; Convex’s own governance token (vlCVX, “vote-locked CVX”) decides how to allocate the veCRV votes.

Result: Curve’s governance is now decided by Convex’s governance. Convex aggregated >50% of veCRV at peak. Vote on Curve gauges = vote on Convex (vlCVX) = “the Convex Kingmaker”.

Auditor angle: when auditing a Curve-integrated protocol, the trust chain is:

  1. Protocol → Curve pool design → Curve governance → Convex governance → vlCVX holders.
  2. A protocol that depends on a Curve pool’s gauge weight depends on vlCVX governance.
  3. Compromise of Convex governance (or its meta-governance) is a compromise of the protocol’s reward distribution.

7.4 Bribe markets — Votium, Hidden Hand, Warden

Once Convex aggregated veCRV power, a market emerged: protocols pay vlCVX holders to vote a particular way. Platforms:

PlatformMechanismWhat it markets
VotiumVote-claim merkle drop after gauge vote roundvlCVX (Convex), and other ve-systems
Hidden Hand (Redacted)Bribe marketplace for multiple protocolsvlCVX, veBAL, etc.
WardenDelegated voting power rentalveCRV directly

Mechanism: protocol P deposits, say, $100k of P’s own token as a “bribe” on a gauge. vlCVX holders vote that gauge gets X% emissions; in proportion to their vote weight, they receive P tokens. P gets CRV emissions to its pool, vlCVX holders get free P, the market clears.

7.5 Is bribing an attack?

The auditor’s view: it depends on what the protocol disclosed and whether holders consented to a market for their votes.

  • If the protocol marketed vlCVX as a “long-term, mission-aligned governance token”, bribing is a covert misalignment — closer to a vulnerability.
  • If the protocol explicitly designed for bribes (Frax, Curve, most ve-systems by 2026), bribing is the system’s intended liquidity-coordination mechanism — a feature.

But in either case, bribes shift the economic gravity of vote outcomes. Auditing a protocol that depends on a particular gauge weight is auditing the bribe market that decides that weight. Quantify: what would it cost to flip the gauge for one week? If that cost is less than the value-at-risk in the gauge, you have an attack surface.

7.6 The Curve auditor’s checklist for ve-protocols

  • Lock-time bounds enforced (min 1 week, max 4 years).
  • Decay formula correct; no rounding edge that allows voting weight > locked weight.
  • No emergency-unlock function reachable without long delay.
  • Smart-contract whitelist for locking gated by governance, with rotation procedure.
  • If protocol integrates with Convex / Yearn / Aura, document the meta-governance dependency.
  • If protocol’s revenue depends on gauge weight, model the bribe-flip cost.

8. Off-Chain Voting (Snapshot) + On-Chain Execution

8.1 Why off-chain voting at all

Gas costs make on-chain voting expensive. For a protocol with 50k token-holders and a 1-month vote cadence, on-chain voting alone could cost the community millions per year in gas. Off-chain voting via signed messages is free.

Snapshot (snapshot.org) is the dominant venue:

  • Voters sign a typed message: “I vote X on proposal Y with weight W (snapshot at block B)“.
  • Snapshot’s backend tallies signatures.
  • No on-chain footprint at vote time.

The voting power is computed from an on-chain snapshot (e.g., balance at block B from an Ethereum archive node), so token-balance integrity holds. But execution is off-chain by default — Snapshot proposals are “signals”, not actions.

8.2 The bridge to on-chain action: SafeSnap + Reality.eth

To make Snapshot votes binding on a Gnosis Safe, the SafeSnap module wraps Reality.eth:

flowchart LR
  S[Snapshot vote passes] --> R[Reality.eth question opened<br>"Did proposal X pass with payload Y?"]
  R -- 24h cooldown + bond --> Verdict[Resolved: Yes/No]
  Verdict -- Yes --> Safe[Gnosis Safe module]
  Safe -- executeProposal --> Targets[Targets execute]
  Verdict -- No / disputed --> X[No execution]
  
  style R fill:#ffe0b3
  style Safe fill:#cce5ff

Reality.eth is an oracle: anyone can post an answer to the question, backed by a bond. Anyone else can dispute by posting a higher bond. After a cooldown with no further dispute, the answer is final.

If the final answer is Yes, the SafeSnap module on the Gnosis Safe permits anyone to call executeProposalWithIndex(...) and triggers the multisend payload.

8.3 Trust assumptions in SafeSnap

The full trust chain:

LayerTrust assumptionFailure mode
Snapshot votingOff-chain signatures correctly counted; balance lookup from archive node honestSnapshot UI compromise; archive-node lie (unlikely but possible)
Reality.eth resolutionBonded answerers act honestly under economic incentiveBond too low: spam-Yes wins by default
Cooldown windowWatchers see the question and dispute if neededAll watchers asleep / busy
Safe module executionAnyone can trigger after resolution(Open execution is fine if resolution is correct)
Gnosis Safe itselfMultisig signers honest(For the SafeSnap module’s owner-Safe, signers control upgrade)

Auditor checklist:

  • Reality.eth bond amount ≥ economic value of the worst-case proposal.
  • Cooldown window ≥ time for watchers to react (typically 24–72h).
  • Snapshot space’s admin keys (controller field) are not a single EOA.
  • The Safe module’s executeProposal correctly recomputes the proposal hash on-chain and refuses to execute mismatched payloads.
  • No backdoor: e.g., Safe owner who can disable the module mid-flow and run a parallel transaction.

8.4 Tally and on-chain governance frontends

For on-chain Governors (OZ Governor, Bravo), Tally (tally.xyz) is the dominant frontend. It:

  • Reads proposals from the Governor.
  • Decodes calldata against ABIs.
  • Displays vote tallies, delegations, governance history.
  • Allows wallet-connected voting.

Tally is not a security boundary — the Governor is on-chain — but it’s a trust assumption for users. A malicious Tally clone (typosquatting domain, MITM, etc.) could show a different proposal description than the actual on-chain payload. Always decode the calldata yourself for high-stakes votes.

For competitive governance attacks (Compound 289, Tornado Cash), the attackers exploited the gap between what Tally displayed (boring/innocuous) and what the calldata actually did (transfer all the things). See §11 (lab).


9. Multisig Governance — When It’s Right, When It’s a Time Bomb

9.1 When multisig is appropriate

ScenarioMultisig fit
Protocol pre-launch / early bootstrapHigh — agility for bug fixes; community too small for token governance
Emergency pause guardianHigh — fast response; limited scope
Treasury operations (paying contractors)Medium — fits operational reality, but transparency matters
Upgrade authority for high-TVL protocolLow — should be timelock-gated and ideally governance-decided
Long-term governance after maturityLow — increasingly seen as centralization debt

9.2 Threshold and M-of-N considerations

N (signers)M (threshold)Notes
32Bare minimum; one compromised + one bribed = pwn
53Common for small DAOs; one compromised survives
74Common emergency council; allows for 3 unreachable signers
9–15~60%OP Stack Security Council size, Lido’s M-of-N variants

Trade-off:

  • Higher N: more resilient to individual compromise; harder to reach quorum quickly.
  • Higher M/N ratio: harder to attack via N - M signer compromise; harder to operate in real-time.
  • Lower M: faster operations; weaker security.

The Optimism Security Council uses a multi-signer model with multiple regions/jurisdictions to make coordinated compromise harder.

9.3 Signer hygiene — what an auditor must enumerate

Every multisig signer is a trust assumption per individual. The audit deliverable should include a table:

SignerIdentity (legal / pseudonymous)Key custody (hardware / hot / MPC)Activity historyConflicts of interest
0x…Foundation board member, namedLedger hardware, multi-jurisdictionalActive in 100% of historical signsNone disclosed
0x…Pseudonymous (twitter handle)UnknownActive in 20% of historical signsHolds significant token position

If any cell reads “unknown” — flag.

9.4 Common multisig findings

  • Single hot signer: one signer’s key is on a server, the rest on hardware. Server compromise + N-1 social engineering = pwn.
  • All signers in same jurisdiction: legal subpoena risk.
  • Signers never rotate: old keys never cycled; if any was ever leaked, it’s still authorized.
  • No off-chain coordination signal: signers don’t have a verified communication channel; spear-phishing of one signer simulating other signers asking for the multisig URL is easy.
  • Safe modules with unaudited authority: SafeSnap, Zodiac Reality, Roles modifier — each adds an authorization path the auditor must trace.

9.5 Radiant Capital 2024 — the cautionary tale

[verify] In October 2024, the Radiant Capital multisig was compromised via malware on signers’ devices that altered the displayed payload while the signers approved a benign-looking transaction. Attacker drained ~$50M. The bug wasn’t in the Safe — Safe worked correctly. The bug was in the signer-side UX: signers couldn’t distinguish what was on-screen from what was being signed.

Audit takeaway: signer UX is part of the threat model. Recommend hardware wallets with full payload verification, transaction simulation (Tenderly, Phalcon) before signing, and out-of-band confirmation that the displayed payload matches the intended action.


10. Emergency Powers, Pause Guardians, and the Trust Problem

10.1 The pause guardian

A simple emergency role: an address (typically a small multisig) can pause the protocol. Pausing is non-destructive — it freezes activity until governance can act.

Done well:

  • Guardian can only pause, not change parameters.
  • Pause unlocks automatically after a max-duration (e.g., 7 days) to prevent permanent freeze by a hostile guardian.
  • Unpause requires governance (full vote), not just guardian.

Done poorly:

  • “Pause” is implemented by setting a flag that also disables the upgrade-revert path — pause + delete impl = brick.
  • Guardian role can be re-granted to itself indefinitely, creating an unaccountable role.
  • No max-duration; permanent pause possible.

10.2 Emergency upgrade

Beyond pause: some protocols give an emergency multisig the ability to upgrade contracts without going through the timelock or governance. This is the “we’ll need a hot patch” path.

The trade-off:

  • Without it: a critical bug discovered post-deploy waits 2–7 days for governance to fix, during which the bug is exploitable.
  • With it: the emergency role is effectively a parallel governance with no delay — compromise = total loss.

The auditor’s stance: emergency upgrade powers are appropriate only when (a) the role is held by a high-trust, diverse multisig, (b) the action is broadcast and observable, and (c) governance can revoke the role. Otherwise, the “emergency” is a permanent backdoor.

10.3 The MakerDAO model — multiple guardian roles

MakerDAO has historically used a layered emergency response:

  • Emergency Shutdown: any MKR holder with enough threshold can trigger global settlement.
  • GSM (Governance Security Module): delays executive votes for a configurable window (historically 24–72h).
  • Multiple authorized “wards” on specific contracts: each with documented scope.

The result is that no single party can act unilaterally on a high-impact change, but the protocol can still respond to emergencies via a structured path. Audit angle: enumerate every ward authorization; understand the upgrade path of each ward’s scope.


11. Parameter Risk

Beyond upgrades, governance changes parameters. Many high-impact incidents in DeFi history have been a single bad parameter change executed through a normal governance proposal — sometimes due to mistake, sometimes due to attack, sometimes due to insufficient review.

11.1 The parameter audit checklist

Parameter classExamplesFailure modeAudit signal
Oracle feedsetOracle(token, feed)Switch to attacker-controlled feed; drain via mis-priced liquidationsRange check feed identity; ideally feed allowlist
Oracle heartbeat / stalenesssetHeartbeat(seconds)Disable staleness check; use ancient pricesFloor on heartbeat values
Fee parametersetFee(bps)100% fee = effective freeze; 0% = drain via no-fee arbitrageHard upper/lower bounds in setter
Collateral factor / LTVsetLTV(asset, bps)Set 99% LTV; immediate insolvencyHard upper bound (e.g., 85%)
Reward distributionsetRewardRate(uint)Set absurdly high; drain rewards poolCap on rate as % of treasury
Pausepause()Permanent freezeMax-duration enforcement
WhitelistaddCollateralAsset(addr)Whitelist attacker’s malicious tokenMulti-step approval (queue + delay)

The pattern: every parameter setter is a code path with the same audit rigor as a state-mutating function. Range-check, sanity-check, time-lock-gate.

11.2 Oracle parameter risk — the highest-stakes

A lending protocol’s oracle is the source of truth for liquidations. Whoever can change the oracle can:

  1. Set the oracle to report a price that liquidates everyone.
  2. Set the oracle to report a high price for the attacker’s collateral, then borrow against it.
  3. Set the oracle to report a low price for the protocol’s debt, making liquidations unprofitable, then default.

Audit angle: trace every setOracle, setPriceFeed, addAsset call. Verify each is timelock-gated and ideally has an allowlist of acceptable feeds (Chainlink-only, with verified aggregator address, with minimum heartbeat).

11.3 The “innocuous” parameter change

The Compound Proposal 289 attack used this pattern. The proposal looked like a routine reward distribution change. The actual effect was to send $24M of treasury COMP to a yield strategy controlled by the attacker. The vote nearly passed.

Auditor lesson: a 51% vote on a $24M move is not “decentralized governance”. It is a coin-flip on protocol solvency. The Compound governance design — quorum 4%, threshold 25k COMP, voting delay 2 days, voting period 3 days — was operating as intended. The failure was the gap between what the proposal claimed and what it did. No timelock or quorum saves you from a winning vote on a malicious payload that nobody read carefully enough.


12. Audit Angles for Governance — the Master Checklist

The deliverable for any governance review.

12.1 Privileged role inventory

For every contract in the protocol:

  • Enumerate every onlyRole, onlyOwner, onlyGovernor, onlyTimelock modifier.
  • Map each modifier to the address(es) currently holding that role.
  • For each role-holder, document: identity, custody, rotation path, removal path.
  • Flag any role held by an EOA (vs multisig or timelock).
  • Flag any role with no removal path.

12.2 Proposal calldata review

For every governance proposal in the queue (or hypothetical proposals from the test suite):

  • Decode every target/value/calldata triplet.
  • For each call: what does it do? Is it timelock-gated? Is it reversible?
  • Are any of the calls modifying privileged roles (e.g., adding a new admin, changing the timelock minDelay, setting a new oracle)?
  • Are any of the calls delegatecall-style executions that bypass normal access control?

12.3 Timelock bypass paths

  • Direct admin functions on dependencies (Chainlink aggregator, etc.).
  • Module / plugin systems (Safe modules, Aragon apps).
  • Emergency upgrade roles.
  • Diamond facet swaps without timelock.
  • “Pause” functions that can also rewrite logic.
  • setMinDelay(0) reachability.

12.4 Upgrade authority

  • Single multisig? Multisig + timelock? Governance + timelock? Governance + multisig veto?
  • What’s the worst-case attacker action under upgrade authority? Document.
  • Is there an immutable, non-upgradeable core? (Some protocols separate “immutable accounting logic” from “upgradeable routing/oracle”.)

12.5 Snapshot timing for vote power

  • Voting delay ≥ 1 day for high-TVL DAOs.
  • Voting power read from historical checkpoint (getPastVotes), not current balance.
  • Token implements ERC20Votes or equivalent checkpoint mechanism.
  • Snapshot block is set at proposal creation, not at vote time.

12.6 Quorum and threshold sanity

  • Quorum % is non-zero and meaningful (≥ 2% of supply typically).
  • Proposal threshold prevents spam (typically 0.25–1% of supply).
  • Voting period ≥ 3 days for high-TVL DAOs.
  • Timelock delay ≥ 2 days for high-TVL DAOs.

12.7 Trust assumption disclosure

  • Centralization risks documented in protocol docs and in the audit report appendix.
  • Multisig signers identified.
  • Governance attack cost modeled (cost to acquire enough voting power, including bribery / flash-loan + delegation paths).

13. Workflow — How an Auditor Reviews a Live Governance Proposal Post-Deploy

This is the day-to-day work for auditors with governance retainers.

13.1 The pre-proposal phase

  1. Subscribe to forum discussion (Gov forum, Discord governance channels) for the protocol.
  2. When a proposal is announced, read the rationale post before reading any calldata.
  3. Note: what does the proposal claim to do? What’s the political context?

13.2 The on-chain proposal phase

  1. Pull the proposal from Tally or directly from the Governor: getActions(proposalId).
  2. For each action (target, value, signature, calldata):
    • Resolve target to a known contract or flag unknown.
    • Decode calldata using the target’s ABI.
    • Match decoded function call to documentation: does it match what the rationale claimed?
  3. For each parameter change:
    • Is the new value within reasonable bounds?
    • Is the change reversible (can a future proposal undo it)?
    • Are dependent contracts also being updated consistently?

13.3 The privileged-role check

  1. Does the proposal grant or revoke any roles? (Tracing grantRole / revokeRole calls.)
  2. Does the proposal change the timelock minDelay, governor settings, quorum, or threshold?
  3. Does the proposal change the proxy implementation? (Tracing upgradeTo / upgradeToAndCall.)

13.4 The simulation phase

  1. Fork the chain at the current block (anvil --fork-url $RPC).
  2. Impersonate the proposer; warp to post-voting-end; execute the proposal in the fork.
  3. Verify the post-state matches expectations from the proposal.
  4. Test invariants: did total supply change? Did treasury drain? Did any unexpected role grants occur?
# Foundry fork + state diff
anvil --fork-url $RPC_URL --fork-block-number $BLOCK
forge script script/SimulateProposal.s.sol --rpc-url http://localhost:8545 --broadcast

13.5 The deliverable

A one-page proposal review:

  • Summary: 2 sentences on what the proposal does.
  • Claims vs reality: any deviations.
  • Privileged role changes: enumerated.
  • Severity: severity tag if any concern (Critical, High, Medium, Low, Informational).
  • Recommendation: vote For / Against / Abstain / Delay (asking for clarification).

Auditors with multiple governance retainers will publish these as part of their public newsletter / Substack — it’s an excellent way to build a public track record.


14. Lab

The labs are demanding this week. Budget the time.

14.1 Lab structure

~/web3-sec-lab/wk14/
├── 01-oz-governor-flow/
├── 02-flash-loan-governance/
├── 03-proposal-calldata-audit/
└── 04-centralization-risks-appendix/

14.2 Lab 1 — Deploy a full OZ Governor + Timelock + ERC20Votes stack

Goal: build the whole thing locally, submit a proposal, vote on it, queue it, execute it. By the end, you can mentally rehearse the lifecycle without re-reading docs.

Setup:

mkdir -p ~/web3-sec-lab/wk14/01-oz-governor-flow && cd ~/web3-sec-lab/wk14/01-oz-governor-flow
forge init --no-commit
forge install OpenZeppelin/openzeppelin-contracts

Token (src/GovToken.sol):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
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";
import {Nonces} from "@openzeppelin/contracts/utils/Nonces.sol";
 
contract GovToken is ERC20, ERC20Permit, ERC20Votes {
    constructor(address initialHolder)
        ERC20("Gov Token", "GOV")
        ERC20Permit("Gov Token")
    {
        _mint(initialHolder, 1_000_000e18);
    }
 
    function _update(address from, address to, uint256 value)
        internal override(ERC20, ERC20Votes) { super._update(from, to, value); }
 
    function nonces(address owner)
        public view override(ERC20Permit, Nonces) returns (uint256)
    { return super.nonces(owner); }
}

Timelock: deploy TimelockController directly. minDelay = 2 days.

Governor (src/MyGovernor.sol):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
import {Governor} from "@openzeppelin/contracts/governance/Governor.sol";
import {GovernorSettings} from "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import {GovernorCountingSimple} from "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import {GovernorVotes} from "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import {GovernorVotesQuorumFraction} from "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import {GovernorTimelockControl} from "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
import {IVotes} from "@openzeppelin/contracts/governance/utils/IVotes.sol";
import {TimelockController} from "@openzeppelin/contracts/governance/TimelockController.sol";
 
contract MyGovernor is
    Governor, GovernorSettings, GovernorCountingSimple,
    GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl
{
    constructor(IVotes _token, TimelockController _timelock)
        Governor("MyGovernor")
        GovernorSettings(7200 /* 1 day */, 50400 /* 7 days */, 1000e18)
        GovernorVotes(_token)
        GovernorVotesQuorumFraction(4)
        GovernorTimelockControl(_timelock) {}
 
    // Required diamond-inheritance overrides
    function votingDelay() public view override(Governor, GovernorSettings) returns (uint256) { return super.votingDelay(); }
    function votingPeriod() public view override(Governor, GovernorSettings) returns (uint256) { return super.votingPeriod(); }
    function quorum(uint256 timepoint) public view override(Governor, GovernorVotesQuorumFraction) returns (uint256) { return super.quorum(timepoint); }
    function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { return super.proposalThreshold(); }
    function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { return super.state(proposalId); }
    function proposalNeedsQueuing(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (bool) { return super.proposalNeedsQueuing(proposalId); }
    function _queueOperations(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
        internal override(Governor, GovernorTimelockControl) returns (uint48)
    { return super._queueOperations(proposalId, targets, values, calldatas, descriptionHash); }
    function _executeOperations(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
        internal override(Governor, GovernorTimelockControl)
    { super._executeOperations(proposalId, targets, values, calldatas, descriptionHash); }
    function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
        internal override(Governor, GovernorTimelockControl) returns (uint256)
    { return super._cancel(targets, values, calldatas, descriptionHash); }
    function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { return super._executor(); }
}

Test (test/GovernorFlow.t.sol): in one test, walk the full lifecycle:

function test_full_governance_flow() public {
    // 1. Setup: deploy token, timelock, governor; mint to holders; delegate
    // 2. Holder1 calls propose() with a payload
    // 3. Advance blocks by votingDelay
    // 4. Holders cast votes
    // 5. Advance blocks by votingPeriod
    // 6. Call queue()
    // 7. Advance time by timelock minDelay
    // 8. Call execute()
    // 9. Assert target's state changed as expected
}

The exercise: write this test from scratch without copy-pasting. The first run will reveal which Governor functions you didn’t fully understand. Iterate until green.

14.3 Lab 2 — Reproduce a flash-loan governance attack

Goal: write a vulnerable governor that reads voting power from live balance, then attack it.

Setup:

cd ~/web3-sec-lab/wk14 && mkdir 02-flash-loan-governance && cd 02-flash-loan-governance
forge init --no-commit

Vulnerable token + governor (src/VulnGov.sol):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
contract VulnToken {
    mapping(address => uint256) public balanceOf;
    uint256 public totalSupply;
    
    function mint(address to, uint256 amount) external { balanceOf[to] += amount; totalSupply += amount; }
    function transfer(address to, uint256 amount) external returns (bool) {
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }
}
 
contract VulnGovernor {
    VulnToken public immutable token;
    address public treasury;
    address public owner;
    uint256 public quorumPercent = 50;
    
    constructor(VulnToken _token, address _treasury) {
        token = _token;
        treasury = _treasury;
        owner = msg.sender;
    }
    
    // VULNERABILITY: reads live balance, not historical
    function emergencyDrain(address to) external {
        require(token.balanceOf(msg.sender) * 100 >= token.totalSupply() * quorumPercent, "insufficient power");
        owner = to;  // attacker takes ownership
    }
}

Flash-loan attack (test/AttackGov.t.sol):

function test_flash_loan_governance_attack() public {
    // 1. Deploy VulnToken; mint 10M to innocent_holder
    // 2. Deploy VulnGovernor; record initial owner
    // 3. Attacker has 0 tokens
    // 4. Simulate flash loan: mint 20M tokens to attacker (simulating flash-loan inflow)
    // 5. Attacker calls emergencyDrain(attacker)
    // 6. Repay flash loan: burn 20M from attacker
    // 7. Assert governor's owner is now attacker
}

Patch: replace VulnToken with ERC20Votes; replace emergencyDrain with a proposal that uses getPastVotes(snapshot). Re-run; assert the test now fails (attack fails because attacker’s getPastVotes(snapshot) is 0).

This is the cleanest hands-on demonstration of why ERC20Votes is non-negotiable for any token-voting governance.

14.4 Lab 3 — Audit a real governance proposal

Goal: pick a live proposal from a major DAO and produce a real one-page proposal review.

Procedure:

  1. Browse Tally.xyz; pick a recent proposal from Compound, Uniswap, Arbitrum, Optimism, or any OZ-Governor DAO.
  2. Get the proposal ID and pull on-chain data:
# Example for OZ Governor
cast call $GOVERNOR "proposalDetails(uint256)" $PROPOSAL_ID --rpc-url $RPC
# Or for Bravo
cast call $GOVERNOR "getActions(uint256)" $PROPOSAL_ID --rpc-url $RPC
  1. For each (target, value, signature, calldata):

    • Resolve target on Etherscan; get ABI.
    • Decode calldata: cast 4byte-decode $CALLDATA or use Tenderly’s decoder.
    • Write 1 sentence on what the call does.
  2. Identify privileged role changes (grantRole, setOwner, upgrade*, setOracle, setFee*, setLTV, etc.).

  3. Produce the deliverable: a markdown one-pager:

# Proposal Review: [Protocol] #[ID]
 
## Summary
[2 sentences plain English]
 
## Calldata breakdown
| # | Target | Function | What it does |
|---|--------|----------|--------------|
| 1 | 0x... | setPriceOracle(0x...) | Switches the BTC oracle from Chainlink mainnet to a new aggregator |
| 2 | ... | ... | ... |
 
## Privileged role changes
- [list any role grants/revokes/changes]
- [list any timelock or governance setting changes]
 
## Severity / Recommendation
[Critical / High / Medium / Low / Informational]
[Vote For / Against / Abstain / Request clarification]
 
## Open questions
- [things you couldn't determine from on-chain data alone]

Stretch: simulate the proposal on a mainnet fork (Foundry), execute it, and assert the post-state matches your prediction.

14.5 Lab 4 — Centralization Risks & Governance appendix

Goal: write a section of an audit report.

Procedure:

  1. Pick a small protocol: e.g., a small DeFi yield aggregator, an NFT marketplace, a fork of Compound/Aave. Ideally something with verified Etherscan source ≤ 2000 lines.

  2. Read every Solidity contract; enumerate every:

    • onlyOwner / Ownable function.
    • onlyRole(X) function.
    • onlyTimelock / onlyGovernor function.
    • _authorizeUpgrade function.
    • Direct-set parameter function (admin-settable price feeds, fee tiers, etc.).
  3. For each privileged function, document:

### [Function name]
 
- **Contract**: 0x...
- **Role required**: onlyOwner / onlyRole(...)
- **Current role holder**: 0x... (e.g., Gnosis Safe with 3-of-5 signers)
- **What it does**: ...
- **Worst-case impact**: ...
- **Mitigation**: timelock / multisig / governance / none
- **Severity tag**: Critical / High / Medium / Low / Informational
 
  1. Aggregate into a “Centralization Risks & Governance” appendix:
# Appendix: Centralization Risks & Governance
 
## Overview
[1 paragraph on the governance architecture]
 
## Privileged roles inventory
[table]
 
## Trust assumptions
[bullet list]
 
## Recommended improvements
[bullet list]

Deliverable: a 2–4 page markdown file. This is the exact format that appears in real audit reports.


15. Anti-Patterns (add to master checklist)

  • Voting power read from current balance instead of getPastVotes (flash-loan-vulnerable).
  • votingDelay = 0 (allows propose + vote in same block).
  • proposalThreshold = 0 (allows spam).
  • quorum = 0 (any vote passes; effectively no quorum).
  • votingPeriod < 1 day for high-TVL protocols.
  • Timelock minDelay < 24h for high-TVL protocols.
  • Timelock DEFAULT_ADMIN_ROLE held by deployer EOA.
  • Timelock CANCELLER_ROLE unset.
  • setMinDelay(uint256) callable without timelock floor.
  • Emergency pause guardian can rewrite logic (not just pause).
  • No max-duration on pause (permanent freeze possible).
  • Privileged role holders not documented in protocol docs.
  • Multisig signers not publicly identified (or all in one jurisdiction).
  • Multisig hot signer with no hardware key.
  • Diamond facet swaps bypass timelock.
  • Oracle parameter setOracle(addr) without allowlist.
  • Fee parameter setFee(bps) without upper bound.
  • LTV parameter setLTV(bps) without upper bound.
  • permit/signature-based proposal voting without ERC-1271 support (excludes AA voters).
  • No on-chain delegation = single point of failure for delegate’s voters.

16. Trade-Offs

DecisionOption AOption BAuditor’s view
Voting modelOn-chain Governor + TimelockOff-chain Snapshot + SafeSnapOn-chain for high-TVL protocols (~$100M+); Snapshot for early-stage / treasury-only. Hybrid (Snapshot signal + Governor execution) is increasingly common.
Voting power sourceLive balanceCheckpointed historical (ERC20Votes)Always checkpointed. Live balance is the Beanstalk bug.
Voting periodShort (3 days)Long (7+ days)3 days minimum for established DAOs; 7 days for high-TVL or contentious topics.
Timelock delay2 days7 days2 days minimum; 7 days for upgrades / treasury moves > 1% of TVL.
QuorumLow (2%)High (10%+)4% is the Compound Bravo default and reasonable. Higher quorums risk permanent gridlock.
Emergency roleNoneMultisig pause-onlyMultisig pause-only with max-duration. No emergency upgrade authority unless absolutely necessary.
BicameralSingle Token HouseToken House + Citizens’ HouseOptimism’s bicameral design is the most-cited check on token-vote plutocracy in 2026. Auditor angle: how are Citizens chosen?
Conviction / quadraticToken voteConviction / quadraticFor grant-funding / public-goods allocation: conviction or quadratic. For protocol-critical decisions: token vote with strong timelock.

17. Quiz (≥80% to advance)

  1. Q: A protocol uses an ERC20 (not ERC20Votes) token for governance, with the governor reading token.balanceOf(voter) at vote time. Severity? A: Critical. Flash-loan governance attack is trivial. This is the Beanstalk bug class. Recommend migrating to ERC20Votes with getPastVotes(snapshot) lookup.

  2. Q: What’s the role of votingDelay in OZ Governor, and what’s its security purpose? A: Delay between propose() and the start of voting. Its purpose is to set the snapshot block for voting power lookup — it’s the block at which ERC20Votes checkpoints are read. Without this delay, an attacker could propose and vote with newly-acquired (e.g., flash-loaned) tokens in the same block.

  3. Q: A TimelockController has DEFAULT_ADMIN_ROLE held by a deployer EOA, with PROPOSER_ROLE held by the Governor. What’s the worst case? A: The deployer can grant themselves PROPOSER_ROLE and EXECUTOR_ROLE, then schedule and execute arbitrary operations. They can also call updateDelay(0) to remove the timelock. The deployer is the actual admin, not the Governor. Critical finding.

  4. Q: What’s the canonical Compound Bravo quorum value, and why is it relevant? A: 400,000 COMP, ≈ 4% of total supply. Relevant because it’s the most-copied governance template — many forks inherit this without examining whether 4% is appropriate for their token distribution and participation patterns.

  5. Q: A protocol uses Snapshot for off-chain voting, with SafeSnap + Reality.eth for on-chain execution. What are the trust assumptions you must enumerate in the audit? A: (a) Snapshot’s tally of off-chain signatures, (b) Reality.eth oracle resolution with bond economics, (c) cooldown window length, (d) the Safe module’s correct payload verification, (e) the Safe owners’ integrity, (f) the Snapshot space admin.

  6. Q: Curve’s veCRV is supposedly flash-loan-resistant. Why? A: Locking is for minimum 1 week and non-reversible (no early unlock). You can’t lock and unlock in one transaction, so flash loans cannot acquire and release veCRV in the same block.

  7. Q: A DAO uses OZ Governor + TimelockController with all proper settings. Their tokenomics integrates with Convex (via cvxCRV-style wrapper). Identify the meta-governance risk. A: Voting power on the protocol’s own gauges (or governance, if the protocol’s governance is mediated via locked tokens) is effectively delegated to the wrapper protocol (Convex), and thus to that wrapper’s governance token holders (vlCVX). Compromise of the wrapper’s governance compromises the protocol. Document the meta-governance chain in the trust-assumptions appendix.

  8. Q: Why is the Beanstalk attack different from a typical reentrancy or oracle attack? A: It exploited the governance system itself — the attacker did not exploit a bug in any business-logic contract. They executed an “authorized” malicious proposal by buying voting power for one block via flash loan. The bug was in the governance design: voting power read from live balance, and emergencyCommit allowed execution after a 1-day proposal-eligibility delay (no execution timelock).

  9. Q: Compound Proposal 289 (2024) passed with a 51/49 split, attempting to move $24M to a strategy controlled by a single delegate. Was this a “bug”? A: No — the governance contracts worked as designed. The “bug” was in the governance design: 4% quorum + simple majority + 25k COMP threshold + 3-day vote means a coordinated delegate group with sufficient COMP can win narrowly. The lesson is that governance design must consider not just attack cost but also who shows up and reads proposals. Apathy is a design vulnerability.

  10. Q: A proposal includes the call timelock.updateDelay(0). The current timelock delay is 2 days. What’s your audit assessment? A: Critical-severity finding. The proposal’s effect is to remove the timelock’s protection. If passed, subsequent proposals can execute instantly. Recommend: hard floor on minDelay (e.g., ≥ 1 day) enforced in the contract, OR a separate higher-quorum vote required for any change to minDelay, OR explicit social-process review of any proposal that touches governance parameters.


18. Week 14 Deliverables

  • All four labs completed; tests passing.
  • OZ Governor + Timelock + ERC20Votes deployed locally with proposal → vote → queue → execute flow.
  • Flash-loan governance attack reproduced on a vulnerable governor; ERC20Votes patch verified to defeat it.
  • One-page proposal review of a real on-chain proposal (Tally / Etherscan) submitted to personal notes.
  • Centralization Risks & Governance appendix for a small chosen protocol (2–4 pages).
  • Master audit checklist updated with this week’s items.
  • Notes file: written walkthrough of “If I were governance-attacking [protocol X], how would I approach it?“

19. Where this leads

Next week: Tuan-15-Audit-Methodology-Tooling. You’ll integrate everything from Weeks 1–14 into a methodology — the actual process by which a professional auditor takes an unknown codebase, scopes it, threat-models it, runs the tooling (Slither, Echidna, Medusa, Halmos, Certora), reviews manually, and produces a draft of findings.

The shift in mindset is: this week, governance was a specific subsystem to audit. Next week, governance is one module of a larger audit workflow that you apply to any protocol that lands on your desk. Phase 5 (Weeks 15–16) is where the technical knowledge of Phases 1–4 becomes a sellable service.

Then, in Week 16 (Tuan-16-Report-Writing-Capstone), the capstone audit will deliberately include a governance surface — you will be evaluated, in part, on whether your report’s “Centralization & Governance” appendix is at the quality bar a real audit firm would publish.


Last updated: 2026-05-16 See also: Roadmap · References · MOC-Web3-Security-Mastery · Case-Beanstalk-Governance-2022 · Tuan-12-Wallet-AA-Key-Management · Tuan-15-Audit-Methodology-Tooling