On-chain governance is one of the most underaudited attack surfaces in DeFi. Teams spend weeks stress-testing AMM math and lending curves, then ship a Governor.sol with a 24-hour timelock and call it a day. The attacker reads the same contract and starts calculating how many tokens they need to borrow for one block.
This article breaks down every major governance attack class, shows concrete Solidity patterns for both the vulnerability and the fix, and ends with a production-ready security checklist.
The Threat Model
Every DAO proposal is a transaction with system-level permissions. Every governance contract is a potential backdoor. A successful governance attack does not require breaking cryptography or finding a buffer overflow — it requires understanding the rules of the system better than the people who wrote them.
Most teams secure their lending pools, AMMs, or vaults, then assume the DAO is safe because it uses OpenZeppelin’s Governor contracts. That is a dangerous blind spot. Your governance stack controls upgrade paths, treasury outflows, and protocol parameters.
The attack surface covers seven distinct vectors:
- Flash loan voting power acquisition
- Snapshot vs. execution block timing manipulation
- Quorum threshold gaming
- Timelock bypass patterns
- Guardian / admin key compromise
- Proposal spam and griefing
- Malicious proposal logic (the Trojan horse)
Let’s go through each one.
1. Flash Loan Governance Attacks
The Mechanism
A flash loan attack exploits the unique properties of flash loans — uncollateralized loans that must be borrowed and repaid within a single transaction. This characteristic allows attackers to manipulate market variables and profit without the need for upfront capital.
Applied to governance, the attack is devastatingly simple: in decentralized governance, decision-making power often correlates with token ownership. Attackers use flash loans to acquire large amounts of governance tokens temporarily, influencing decisions in their favor — for example, proposing and passing malicious changes to the protocol’s rules.
Beanstalk: The $182M Case Study
The Beanstalk exploit used a flash loan to seize control of its governance system. By temporarily acquiring significant voting power, the attacker approved a proposal to transfer $182 million in assets to their wallet. After repaying the flash loan, they retained a profit of $80 million. This attack exposed the dangers of weak governance mechanisms in DeFi protocols.
The key insight: this case reveals how flash loans can exploit vulnerabilities not just in code, but in process and design. The governance was designed for agility but failed to consider a threat model where an attacker could acquire a temporary super-majority.
Vulnerable Pattern
// VULNERABLE: Voting power read at current block
contract VulnerableGovernor {
IERC20 public token;
struct Proposal {
uint256 id;
address proposer;
bytes callData;
uint256 forVotes;
uint256 againstVotes;
bool executed;
}
mapping(uint256 => Proposal) public proposals;
uint256 public proposalCount;
// ❌ CRITICAL: reads balance at execution time, not snapshot time
function castVote(uint256 proposalId, bool support) external {
uint256 votes = token.balanceOf(msg.sender); // current balance!
Proposal storage p = proposals[proposalId];
if (support) {
p.forVotes += votes;
} else {
p.againstVotes += votes;
}
}
// Attacker flow:
// 1. Flash borrow 10M tokens
// 2. castVote() with 10M votes
// 3. Execute proposal in same tx
// 4. Repay flash loan
}
Hardened Pattern: Checkpoint-Based Voting
// SAFE: ERC20Votes checkpoint pattern
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
contract HardenedGovernor is Governor, GovernorVotes {
constructor(IVotes _token)
Governor("HardenedGovernor")
GovernorVotes(_token)
{}
// Voting power is read at the PROPOSAL's snapshot block,
// not the current block. Flash loans cannot help here —
// the attacker would need to hold tokens before the proposal
// was even created.
function _getVotes(
address account,
uint256 blockNumber,
bytes memory /*params*/
) internal view override(Governor, GovernorVotes) returns (uint256) {
// getPastVotes reads from EIP-5805 checkpoints
return token.getPastVotes(account, blockNumber);
}
function votingDelay() public pure override returns (uint256) {
return 7200; // ~1 day in blocks: attacker must hold tokens before proposal
}
function votingPeriod() public pure override returns (uint256) {
return 50400; // ~1 week
}
function quorum(uint256 blockNumber)
public
view
override
returns (uint256)
{
// 4% of total supply at snapshot block
return (token.getPastTotalSupply(blockNumber) * 4) / 100;
}
}
The ERC20Votes extension uses a checkpoint tree: every token transfer writes a (blockNumber, votes) pair. getPastVotes(account, snapshotBlock) reads the balance at a historical block. Flash loans do nothing — the borrowed tokens have no history at the snapshot block.
2. Snapshot vs. Execution Block: The Timing Gap
Even with checkpoints enabled, the relationship between the snapshot block and the proposal’s lifecycle creates exploitable windows.
The Three-Block Problem
Block N: Proposal created → snapshot = N
Block N+1: Attacker accumulates tokens (now recorded in checkpoints)
...
Block N+100: Voting opens (using snapshot at block N — attacker has 0 votes)
↑ This is correct behavior ✓
Block N: Proposal created → snapshot = N-1 (off-by-one bug)
Block N-1: Attacker front-runs the proposal creation to acquire tokens
↑ This is exploitable ✗
The snapshot must be at or before the block in which propose() is called. If the snapshot is block.number at proposal creation time and a miner (or validator with MEV) can include the token acquisition and the proposal in the same block, the window exists.
Voting Delay as the Second Defense
contract TimingHardenedGovernor is Governor, GovernorVotes {
// votingDelay is the number of blocks between proposal creation
// and when voting begins. This is the primary anti-flash-loan
// defense for the SNAPSHOT timing.
function votingDelay() public pure override returns (uint256) {
// Minimum recommended: 1 day (7200 blocks at 12s/block on Ethereum)
// High-value protocols: 2–3 days
return 14400; // 2 days
}
// The snapshot block is implicitly block.number at propose() time.
// Users must hold tokens BEFORE the proposal is created AND
// through the votingDelay period.
function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) public override returns (uint256) {
// Optionally: require proposer holds tokens for N blocks
uint256 snapshotBlock = block.number - 1;
require(
token.getPastVotes(msg.sender, snapshotBlock) >= proposalThreshold(),
"Governor: proposer votes below threshold"
);
return super.propose(targets, values, calldatas, description);
}
}
Snapshot Voting (Off-Chain) vs. On-Chain Execution
In off-chain governance, the voting itself does not cost gas fees and does not create blockchain transactions. Instead, it acts as a signaling mechanism. Once the community reaches a clear consensus, developers or a multisig wallet execute the decision on-chain.
This introduces a different timing attack: the Snapshot-to-execution gap. The off-chain vote captures token balances at block X, but the multisig executes at block X+500. An attacker who influences the multisig signers during that gap — or who acquired tokens after the snapshot block — can shape execution while appearing powerless during the vote.
The risk is asymmetric: Snapshot votes are cheap to cast (gasless) but the execution keys are a separate, often weaker security boundary.
3. Quorum Manipulation
Low Quorum = Easy Capture
A quorum is the minimum amount of participation required to pass a vote. For example, a proposal may have 100% support from voters, but if the number of token holders who vote fails to meet the minimum percentage required, then the vote is often automatically canceled.
One of the biggest problems in DeFi governance is low participation. People often want to hold tokens for speculative purposes and may not want to participate in governance because it is time-consuming and/or they do not have a strong view on the proposed change to the protocol.
This creates the quorum manipulation attack: an attacker with 5% of supply can pass a proposal if only 4% of supply is required for quorum and no one else votes.
Dynamic Quorum
// Dynamic quorum that adjusts based on recent participation
contract DynamicQuorumGovernor is Governor, GovernorVotes {
uint256 public constant QUORUM_MIN_BPS = 400; // 4% floor
uint256 public constant QUORUM_MAX_BPS = 2000; // 20% ceiling
// Track participation over last N proposals
uint256[10] private _recentParticipation;
uint256 private _participationIndex;
uint256 private _totalSupplyAtLastProposal;
function quorum(uint256 blockNumber)
public
view
override
returns (uint256)
{
uint256 totalSupply = token.getPastTotalSupply(blockNumber);
// Calculate average participation rate from recent proposals
uint256 avgParticipation = _averageRecentParticipation();
// Quorum = max(MIN, min(MAX, 1.5 * avg_participation))
uint256 dynamicBps = (avgParticipation * 150) / 100;
uint256 quorumBps = _clamp(dynamicBps, QUORUM_MIN_BPS, QUORUM_MAX_BPS);
return (totalSupply * quorumBps) / 10000;
}
function _averageRecentParticipation() internal view returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < 10; i++) {
sum += _recentParticipation[i];
}
return sum / 10;
}
function _clamp(
uint256 value,
uint256 min,
uint256 max
) internal pure returns (uint256) {
if (value < min) return min;
if (value > max) return max;
return value;
}
}
Quorum Threshold Manipulation via Token Supply Inflation
A subtler attack: if the quorum is a percentage of current total supply, an attacker can mint tokens (via a governance proposal!) to inflate supply and make the absolute quorum threshold larger — then burn them after, making future quorums trivially achievable. Always snapshot quorum against getPastTotalSupply(snapshotBlock), never totalSupply().
// ❌ VULNERABLE: quorum against live totalSupply
function quorum(uint256) public view override returns (uint256) {
return (token.totalSupply() * 4) / 100; // manipulable!
}
// ✅ SAFE: quorum against historical snapshot
function quorum(uint256 blockNumber) public view override returns (uint256) {
return (token.getPastTotalSupply(blockNumber) * 4) / 100;
}
4. Timelock Bypass Patterns
The Timelock’s Promise
The power of a timelock lies in what it prevents: rapid, coordinated attacks where a malicious upgrade steals funds before anyone can react. If an attacker somehow captures enough voting power to pass a hostile proposal, the timelock gives the community a window to respond — through governance mechanisms, through social pressure, or through coordinated exits.
MakerDAO pioneered this approach with their executive spell system, where governance-approved changes face a 12-hour delay before activation. Compound took a similar route with their Timelock contract, enforcing a 48-hour delay on all proposal executions.
As of 2024, many governance systems set timelock periods between 24–72 hours; larger treasuries or major upgrades may require longer delays.
Bypass Pattern 1: The Emergency Admin
// VULNERABLE: emergency admin can bypass timelock entirely
contract BypassableTimelock {
address public admin;
address public emergencyAdmin; // ← single point of failure
uint256 public constant DELAY = 2 days;
mapping(bytes32 => uint256) public queue;
function execute(address target, bytes calldata data) external {
// ❌ Emergency path has no delay
if (msg.sender == emergencyAdmin) {
(bool success,) = target.call(data);
require(success, "execution failed");
return;
}
// Normal path checks timelock
bytes32 txHash = keccak256(abi.encode(target, data));
require(queue[txHash] != 0, "not queued");
require(block.timestamp >= queue[txHash] + DELAY, "too early");
delete queue[txHash];
(bool success,) = target.call(data);
require(success);
}
}
If emergencyAdmin is a single EOA, compromising that key bypasses the entire governance architecture.
Bypass Pattern 2: The Proxy Upgrade Race
// VULNERABLE: proxy owner can upgrade without timelock
contract VulnerableProxy is ERC1967Proxy {
address public owner;
// ❌ upgradeToAndCall has no governance check
function upgradeToAndCall(
address newImplementation,
bytes memory data
) external {
require(msg.sender == owner); // no timelock!
_upgradeToAndCallUUPS(newImplementation, data, false);
}
}
The correct pattern sets the TimelockController as the proxy owner:
import "@openzeppelin/contracts/governance/TimelockController.sol";
contract SecureTimelock is TimelockController {
constructor(
uint256 minDelay,
address[] memory proposers,
address[] memory executors,
address admin
) TimelockController(minDelay, proposers, executors, admin) {}
}
// During deployment:
// 1. Deploy SecureTimelock with minDelay = 48 hours
// 2. Set Governor as PROPOSER_ROLE
// 3. Set zero address (open) or specific keepers as EXECUTOR_ROLE
// 4. Transfer proxy ownership to SecureTimelock address
// 5. Renounce TIMELOCK_ADMIN_ROLE from deployer
Minimum Timelock Values
| Protocol Tier | TVL | Minimum Delay | Rationale |
|---|---|---|---|
| Early/testnet | < $1M | 24h | Speed matters, lower stakes |
| Growth | $1M–$50M | 48h | Community needs weekend reaction time |
| Established | $50M–$500M | 72h | Cover cross-timezone awareness |
| Blue-chip | > $500M | 7 days | Allow exit for risk-averse LPs |
| Critical parameters | Any | 14 days | Token emissions, fee caps, admin keys |
Timelocks are necessary but not sufficient. They protect against speed, but they don’t protect against sophistication. A well-funded attacker who controls governance through flash loans or economic coercion could still push through an upgrade given enough time to prepare.
5. Guardian Key Compromise
The Guardian Role
Most protocols deploy a “guardian” or “security council” — a multisig with the ability to pause the protocol, cancel queued proposals, or execute emergency actions without going through the full governance cycle.
Account-based governance refers to setups in which the right to execute a restricted function is accorded to one or multiple EOAs. The holders of these ‘admin keys’ can exclusively call restricted functions. In its extreme form, the right is associated with a single key.
Account-based governance indisputably introduces a significant element of centralized control. Arguments in favour of this architecture include efficiency and reaction speed, particularly in case of emergency updates, where timely reactions are critical.
Real-World Key Compromise
Attacks have combined social engineering, malware, and cloud infrastructure vulnerabilities to bypass multisig approval requirements. Incidents exposed critical governance risks in DeFi, highlighting the need for timelocks, higher multisig thresholds, and continuous monitoring.
Off-chain attacks accounted for 80.5% of stolen funds in 2024, and compromised accounts made up 55.6% of all incidents for that year. The majority of these were private key compromises — not smart contract exploits.
Safe Guardian Architecture
// Three-tier guardian structure
contract GuardianController {
// Tier 1: Emergency pause (single guardian, time-limited)
address public pauseGuardian;
uint256 public constant PAUSE_DURATION = 3 days;
uint256 public pauseExpiry;
bool public paused;
// Tier 2: Proposal cancellation (multisig, no delay)
address public cancellationCouncil; // e.g., 5-of-9 Safe
// Tier 3: Parameter changes (governance + timelock)
// Handled by Governor + TimelockController
modifier notPaused() {
require(!paused || block.timestamp > pauseExpiry, "paused");
_;
}
// Guardian can only pause, not upgrade or drain
function emergencyPause() external {
require(msg.sender == pauseGuardian, "not guardian");
paused = true;
pauseExpiry = block.timestamp + PAUSE_DURATION;
emit EmergencyPause(msg.sender, pauseExpiry);
}
// Auto-unpause: guardian cannot extend pause indefinitely
function unpause() external {
require(
msg.sender == cancellationCouncil ||
block.timestamp > pauseExpiry,
"cannot unpause"
);
paused = false;
pauseExpiry = 0;
}
// Cancel a queued timelock proposal (council only)
function cancelProposal(bytes32 id) external {
require(msg.sender == cancellationCouncil, "not council");
ITimelockController(timelock).cancel(id);
}
// CRITICAL: Guardian CANNOT:
// - Upgrade implementation contracts
// - Transfer treasury funds
// - Mint tokens
// - Change fee parameters
// All above require full governance + timelock
}
Multisig Threshold Analysis
A 6-of-11 signature threshold improves the system’s resilience in the face of collusion or hacks involving signers. Signers are well-known community members and thus have an additional incentive — protecting their reputation — to act honestly. Signers have limited powers and cannot, for example, stop individuals from withdrawing funds from liquidity pools in emergencies.
| Threshold | Security | Liveness | Recommendation |
|---|---|---|---|
| 2-of-3 | Low | High | Development only |
| 3-of-5 | Medium | Medium | Small protocols |
| 5-of-9 | Good | Acceptable | Mid-tier protocols |
| 6-of-11 | High | Lower | Large TVL protocols |
| 7-of-13 | Very High | Low | Critical infrastructure |
Liveness (ability to act quickly) degrades as threshold rises. A 7-of-13 council that cannot reach consensus during an ongoing exploit is worse than no guardian at all. Plan key-holder geography, time zones, and redundant communication channels.
Hardware Security Module (HSM) Pattern
Robust authentication measures — such as hardware security modules (HSMs), multi-factor authentication (MFA), and privileged access controls — are essential to protecting user credentials. For guardian keys specifically:
// Timelocked guardian key rotation — even key changes take time
contract GuardianRotation {
address public currentGuardian;
address public pendingGuardian;
uint256 public rotationAvailableAt;
uint256 public constant ROTATION_DELAY = 7 days;
function initiateRotation(address newGuardian) external {
require(msg.sender == currentGuardian, "not guardian");
require(newGuardian != address(0), "zero address");
pendingGuardian = newGuardian;
rotationAvailableAt = block.timestamp + ROTATION_DELAY;
emit GuardianRotationInitiated(newGuardian, rotationAvailableAt);
}
function acceptRotation() external {
require(msg.sender == pendingGuardian, "not pending guardian");
require(block.timestamp >= rotationAvailableAt, "too early");
address old = currentGuardian;
currentGuardian = pendingGuardian;
pendingGuardian = address(0);
emit GuardianRotated(old, currentGuardian);
}
// If guardian key is suspected compromised:
// Community can cancel the rotation via governance before it executes
function cancelRotation() external onlyGovernance {
pendingGuardian = address(0);
rotationAvailableAt = 0;
}
}
6. Proposal Spam and Griefing
The Tornado Cash Governance Takeover
The most instructive governance attack of 2023 was not a flash loan attack — it was a Trojan proposal.
The threat actor successfully shifted 1.2 million votes to a proposal with deceptive intent on May 20th. This initiative received more than 700,000 legitimate votes, enabling the attacker to fully command Tornado Cash governance.
The attack mechanics: the Tornado Cash exploit relied on the fact that voters would vote for the original, malicious proposal. This additional, malicious functionality was overlooked by many users, who voted for the proposal, causing it to be enacted. The attacker then self-destructed the proposal and modified its code. Once the malicious proposal was executed via delegate call, 10,000 governance tokens were assigned to each address controlled by the attacker for a total of 1.2 million votes.
When executing the proposal, due to the use of the delegatecall instruction, the malicious proposal contract only needed to modify its own storage space to synchronously modify the storage space of the Governance contract. Therefore, the malicious proposal changed the balance of each zombie contract to 10,000, and the storage space of the Governance contract was also modified.
The delegatecall Proposal Vector
// VULNERABLE: Governor executes proposals via delegatecall
// This gives proposal contracts full access to Governor storage
contract VulnerableGovernor {
mapping(address => uint256) public lockedVotes; // storage slot 0
function execute(address proposal) external {
// ❌ delegatecall: proposal code runs in Governor's context
// A malicious proposal can write to lockedVotes[attacker] = 1M
(bool success,) = proposal.delegatecall(
abi.encodeWithSignature("execute()")
);
require(success);
}
}
// MALICIOUS PROPOSAL CONTRACT:
contract MaliciousProposal {
// Storage layout mirrors Governor: slot 0 = lockedVotes
mapping(address => uint256) public lockedVotes;
function execute() external {
// Running in Governor's context via delegatecall:
// writes to Governor's lockedVotes mapping
lockedVotes[0xAttacker] = 1_200_000 ether;
}
}
// SAFE: Use call instead of delegatecall for proposal execution
contract SafeGovernor {
function _execute(
uint256, /* proposalId */
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
bytes32 /* descriptionHash */
) internal override {
for (uint256 i = 0; i < targets.length; i++) {
// ✅ Regular call: proposal cannot modify Governor storage
(bool success, bytes memory result) = targets[i].call{
value: values[i]
}(calldatas[i]);
if (!success) {
assembly {
revert(add(result, 32), mload(result))
}
}
}
}
}
Proposal Spam Defense
contract AntiSpamGovernor is Governor, GovernorVotes {
uint256 public constant PROPOSAL_THRESHOLD_BPS = 100; // 1% of supply
uint256 public constant PROPOSAL_DEPOSIT = 1000e18; // token deposit
mapping(uint256 => uint256) public proposalDeposits;
function proposalThreshold() public view override returns (uint256) {
// Proposer must hold 1% of total supply — a meaningful barrier
return (token.getPastTotalSupply(block.number - 1) * PROPOSAL_THRESHOLD_BPS) / 10000;
}
// Require a slashable deposit on top of threshold
function proposeWithDeposit(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) external returns (uint256) {
token.transferFrom(msg.sender, address(this), PROPOSAL_DEPOSIT);
uint256 proposalId = super.propose(targets, values, calldatas, description);
proposalDeposits[proposalId] = PROPOSAL_DEPOSIT;
return proposalId;
}
// Slash deposit if proposal is cancelled (guardian) or fails by large margin
function _afterExecute(
uint256 proposalId,
address[] memory,
uint256[] memory,
bytes[] memory,
bytes32
) internal override {
uint256 deposit = proposalDeposits[proposalId];
if (deposit == 0) return;
ProposalState s = state(proposalId);
if (s == ProposalState.Succeeded || s == ProposalState.Executed) {
// Refund on success
token.transfer(_proposalProposer(proposalId), deposit);
} else {
// Burn/send to treasury on failure (anti-spam)
token.transfer(treasury, deposit);
}
delete proposalDeposits[proposalId];
}
}
7. The Governance Design Spectrum
Whether operating through on-chain voting, off-chain signaling, or multisig committees, governance design determines who holds power and how that power is exercised.
The Spectrum
FULLY CENTRALIZED FULLY DECENTRALIZED
│ │
Single EOA → Multisig → Multisig+Timelock → Governor+Timelock → Immutable
(owner) (3-of-5) + Snapshot signal (fully on-chain) (no upgrades)
│ │
Fast, fragile Slow, resilient
High capture risk Flash loan vectors
Single key = game over Quorum attacks possible
Fully On-Chain Governance
Since smart contracts handle the execution, there is no room for human error or manipulation once a vote passes. The code does exactly what the community decided. This is the promise.
In DeFi DAOs, authority is hardcoded. That makes them fast, transparent, and brutally final. But also brittle. A malicious proposal that passes quorum can drain the treasury.
Hybrid: Snapshot Signal + Multisig Execution
Yearn’s 6-of-9 multisig executes Snapshot votes for strategy deployments, fee changes, and treasury management — trading off faster execution versus centralization concerns.
This model introduces the Snapshot-execution trust gap: voters trust that the multisig will faithfully execute the winning vote, but there is no on-chain enforcement. The multisig can defect.
The Multisig Governance Model
More common than single-key setups are multi-sig conditions that enforce M-of-N quorums among a group of admin key holders.
Designating a timelock contract address as the “owner” of a governance module is a common pattern among DeFi projects. The canonical secure setup:
// Deployment script: layered governance architecture
async function deployGovernance() {
// 1. Deploy governance token with checkpoints
const token = await GovernanceToken.deploy();
// 2. Deploy timelock (Governor is proposer, anyone can execute after delay)
const timelock = await TimelockController.deploy(
172800, // 2 day minimum delay
[governor.address], // proposers: only the Governor
[ethers.ZeroAddress], // executors: anyone (after timelock)
deployer.address // initial admin (will be renounced)
);
// 3. Deploy Governor
const governor = await Governor.deploy(token.address, timelock.address);
// 4. Deploy Guardian multisig (5-of-9 Safe)
// 4. Deploy Guardian multisig (5-of-9 Safe) — out of scope for this script
// The guardian address is passed to the Governor at deployment
// 5. Configure timelock roles
await timelock.grantRole(await timelock.PROPOSER_ROLE(), governor.address);
await timelock.grantRole(await timelock.EXECUTOR_ROLE(), ethers.ZeroAddress); // open execution
await timelock.revokeRole(await timelock.DEFAULT_ADMIN_ROLE(), deployer.address);
}
Key security properties enforced:
DEFAULT_ADMIN_ROLErenounced — no one can modify timelock roles after deployment- Proposer role held only by Governor — proposals cannot bypass the voting process
- Open execution (
address(0)) — anyone can execute after the delay, preventing griefing by a single executor key - Guardian is a high-threshold multisig — cannot be a single point of failure
Governance Security Checklist
Flash loan resistance
- Voting power snapshot is taken at a block prior to proposal creation
- Proposal delay is long enough that flash-borrowed tokens cannot influence the snapshot
- Token delegation changes have a cooldown before they affect voting power
Quorum and threshold design
- Quorum is expressed as a percentage of circulating supply, not total supply
- Quorum cannot be reached by a single actor under normal token distribution
- High-impact operations (upgrades, parameter changes) have higher quorum requirements than routine proposals
Timelock configuration
- Minimum timelock delay is at least 24 hours for parameter changes
- Minimum timelock delay is at least 48-72 hours for upgrades and fund movements
-
DEFAULT_ADMIN_ROLEon the TimelockController is renounced after deployment - Executor role is either open (
address(0)) or held by a sufficiently decentralized set
Guardian design
- Guardian is a multisig with threshold ≥ 3-of-5 or equivalent
- Guardian can veto but cannot propose or execute autonomously
- Guardian key rotation procedure is defined and tested
- Guardian compromise does not allow fund extraction — only veto power
Proposal spam and griefing
- Proposal threshold (minimum tokens to propose) is high enough to make spam expensive
- Proposer must maintain threshold throughout the voting period (cannot front-run with borrowed tokens)
- There is a limit on simultaneous active proposals, or each proposal is independently executable
On-chain vs multisig spectrum
- The governance model is documented with its security assumptions
- Emergency multisig powers are explicitly scoped (pause only, parameter ranges only, etc.)
- Path to fully on-chain governance is defined if currently using multisig
Post-deployment
- Governance contract ownership has been transferred from deployer EOA
- Deployer EOA has no remaining privileged roles
- All initial parameters are within acceptable ranges (not set to extremes that benefit insiders)