An attacker spent $1,800 on governance tokens and nearly walked away with $1.08 million in user funds. No flash loan. No smart contract bug. No zero-day. Just buying tokens on the open market and submitting a proposal.
This was Moonwell on Moonriver, March 2026. The attacker acquired 40 million MFAM tokens — enough to meet quorum — and proposed transferring admin control of seven lending markets, the comptroller, and the price oracle to their own address. The entire attack setup took 11 minutes.
Moonwell survived because they had a "Break Glass Guardian" — a 2-of-3 emergency multisig that could veto the proposal. Most protocols don't have this. And the ones that do often implement it wrong.
Meanwhile, GreenField DAO wasn't as lucky. In April 2025, an attacker flash-borrowed 9 million GOV tokens, passed a malicious proposal, and drained $31 million from the treasury — all within a single block. Beanstalk's $182 million governance exploit in 2022 used the same pattern with over $1 billion in flash-loaned tokens.
Governance attacks are now the highest-ROI exploit class in DeFi. They're cheap, they're repeatable, and most protocols are still vulnerable. Here's why, and how to fix it.
Why Governance Is DeFi's Softest Target
Three structural problems make governance exploits uniquely dangerous:
1. Voting power is purchasable. Unlike exploiting a reentrancy bug (which requires technical skill) or compromising a private key (which requires access), governance attacks just require capital — sometimes very little capital.
2. Low participation is the norm. Most DAOs see 5-15% voter turnout. When quorum is set at 10% of circulating supply and 85% of tokens are dormant, an attacker needs to outbid... nobody.
3. Timelocks provide false security. A 48-hour timelock sounds protective until you realize most governance token holders don't monitor proposals. The timelock expires, the malicious proposal executes, and nobody noticed.
The Attack Economics
Let's quantify how cheap governance attacks actually are:
Moonwell (March 2026):
Attack cost: $1,800 (bought tokens on open market)
Potential profit: $1,080,000
ROI: 600x
GreenField DAO (April 2025):
Attack cost: ~$0 (flash loan, repaid in same tx)
Actual profit: $31,000,000
ROI: Infinite
Beanstalk (April 2022):
Attack cost: ~$0 (flash loan)
Actual profit: $182,000,000
ROI: Infinite
For context, the average smart contract exploit requires weeks of reverse engineering and a deep understanding of the protocol's internals. A governance attack requires reading the proposal threshold from a public contract and checking token prices on a DEX.
The 5 Governance Attack Vectors
Vector 1: Flash Loan Vote Manipulation
The classic. Borrow governance tokens, vote, return them — all in one transaction.
// VULNERABLE: Governance that reads balanceOf() at vote time
function castVote(uint256 proposalId, bool support) external {
uint256 votingPower = governanceToken.balanceOf(msg.sender); // ← Flash-loanable
_recordVote(proposalId, msg.sender, votingPower, support);
}
Why it works: Any governance system that reads token balance at vote time instead of using historical snapshots is exploitable with flash loans.
Vector 2: Low-Quorum Treasury Drain
Moonwell's pattern. Buy tokens below quorum threshold, submit proposal, wait for timelock to expire while nobody notices.
Required voting power: 40M MFAM (quorum)
Cost of 40M MFAM: $1,800
Treasury value: $1,080,000
Why it works: Token liquidity on secondary chains (Moonriver, Fantom, etc.) is often so thin that quorum-level quantities cost less than a used car.
Vector 3: Proposal Payload Obfuscation
The malicious proposal doesn't say "drain the treasury." It says "Upgrade proxy to new implementation" with a bytecode payload that nobody reads.
// Looks innocent in the proposal description:
// \"MIP-42: Upgrade lending pool to v2.1 for gas optimization\"
// Actual calldata:
function execute() external {
// Step 1: Transfer admin of all markets to attacker
comptroller._setPendingAdmin(0xAttacker);
// Step 2: Accept admin (in a follow-up tx after timelock)
// Step 3: Set price oracle to attacker-controlled contract
// Step 4: Manipulate prices and drain all collateral
}
Why it works: Most governance participants vote based on the proposal title and forum discussion, not the on-chain calldata. Auditing proposal payloads requires Solidity expertise that most token holders don't have.
Vector 4: Delegation Accumulation
Instead of buying tokens, slowly accumulate delegation from inactive holders who delegate to "trusted" community members.
// Attacker builds reputation as an active governance participant
// Gets delegated voting power from passive holders over months
// Eventually accumulates enough delegation to pass proposals solo
Why it works: Delegation is designed for efficiency but creates power concentration. A delegatee with 30% of voting power from passive holders is a single point of failure.
Vector 5: Cross-Chain Governance Desync
For protocols with governance on one chain and execution on another, the finality gap creates a voting power fabrication window.
Chain A (governance): Attacker votes with 10M tokens
Bridge message delay: 15 minutes
Chain B (execution): Attacker has already moved tokens
Result: Voting power counted on Chain A, tokens already
transferred on Chain B — double counting
Why it works: Cross-chain message passing has inherent latency. If governance snapshots aren't synchronized across chains, tokens can be "in two places at once."
7 Defense Patterns That Actually Work
Pattern 1: Snapshot-Based Voting (Mandatory)
Never read token balance at vote time. Always use historical snapshots.
// SAFE: OpenZeppelin Governor with ERC20Votes snapshots
function _getVotes(
address account,
uint256 timepoint,
bytes memory /*params*/
) internal view override returns (uint256) {
// Reads balance at proposal creation block, NOT current block
// Flash loans at vote time are worthless — balance is historical
return token.getPastVotes(account, timepoint);
}
Implementation: Use OpenZeppelin's ERC20Votes extension, which tracks voting power at every block via checkpoints. The Governor contract automatically reads from the snapshot at proposal creation time.
// ERC20Votes automatically checkpoints on every transfer
contract GovernanceToken is ERC20, ERC20Votes {
function _update(address from, address to, uint256 amount)
internal override(ERC20, ERC20Votes)
{
super._update(from, to, amount);
// ERC20Votes._update() writes a checkpoint here
// Future getPastVotes() calls read from this checkpoint
}
}
What this prevents: Flash loan voting (Vector 1) — since the attacker didn't hold tokens at the snapshot block, their voting power is zero.
What this doesn't prevent: Market purchase attacks (Vector 2) if the attacker buys tokens before the snapshot block.
Pattern 2: Vote Escrow with Lock Period
Require tokens to be locked for a minimum period before they carry voting weight.
contract VoteEscrow {
uint256 public constant MIN_LOCK_PERIOD = 7 days;
struct Lock {
uint256 amount;
uint256 lockedAt;
uint256 unlockTime;
}
mapping(address => Lock) public locks;
function lock(uint256 amount, uint256 duration) external {
require(duration >= MIN_LOCK_PERIOD, "Lock too short");
token.transferFrom(msg.sender, address(this), amount);
locks[msg.sender] = Lock({
amount: amount,
lockedAt: block.timestamp,
unlockTime: block.timestamp + duration
});
}
function getVotingPower(address account) external view returns (uint256) {
Lock memory userLock = locks[account];
// No voting power if locked less than MIN_LOCK_PERIOD ago
if (block.timestamp < userLock.lockedAt + MIN_LOCK_PERIOD) {
return 0;
}
// Voting power proportional to remaining lock time (ve-model)
uint256 remaining = userLock.unlockTime > block.timestamp
? userLock.unlockTime - block.timestamp : 0;
return (userLock.amount * remaining) / MAX_LOCK_TIME;
}
}
What this prevents: Both flash loan (Vector 1) and quick market purchase attacks (Vector 2). An attacker must lock capital for 7+ days with no guarantee of success, creating real economic risk.
Trade-off: Reduces participation from casual holders who don't want to lock tokens. Mitigate with a liquid wrapper (veToken staking derivative) that still requires lock commitment.
Pattern 3: Break Glass Guardian (Emergency Veto)
Moonwell's saving grace. A multisig with the power to veto malicious proposals outside normal governance flow.
contract BreakGlassGuardian {
address public immutable GOVERNANCE_TIMELOCK;
uint256 public constant GUARDIAN_THRESHOLD = 2; // 2-of-3
address[3] public guardians;
mapping(bytes32 => uint256) public vetoApprovals;
function vetoProposal(uint256 proposalId) external {
require(isGuardian(msg.sender), "Not a guardian");
bytes32 vetoHash = keccak256(abi.encode(proposalId, "VETO"));
vetoApprovals[vetoHash]++;
if (vetoApprovals[vetoHash] >= GUARDIAN_THRESHOLD) {
// Cancel the proposal in the timelock
IGovernor(GOVERNANCE_TIMELOCK).cancel(proposalId);
emit ProposalVetoed(proposalId);
}
}
// Guardian rotation must go through full governance
// (prevents guardian capture)
function rotateGuardian(uint256 index, address newGuardian)
external
onlyGovernance
{
guardians[index] = newGuardian;
}
}
Critical design decisions:
- Guardians can only veto, never propose or execute — this preserves decentralization
- Guardian rotation goes through normal governance — prevents guardian capture
- Use a 2-of-3 or 3-of-5 threshold — single guardians are a centralization risk
- Guardians should be geographically and organizationally diverse
What this prevents: Any governance attack that relies on timelock expiration (Vectors 2, 3, 4). Even if the proposal passes, guardians can cancel it.
What this doesn't prevent: Attacks that bypass governance entirely (key compromise, contract vulnerabilities).
Pattern 4: Proposal Validation Hooks
Automatically verify that proposal calldata doesn't target critical functions without explicit safeguards.
contract GovernorWithValidation is Governor {
// Functions that require super-majority (>80%) instead of simple majority
mapping(bytes4 => bool) public criticalSelectors;
constructor() {
// Admin transfers always require super-majority
criticalSelectors[bytes4(keccak256("_setPendingAdmin(address)"))] = true;
criticalSelectors[bytes4(keccak256("_setImplementation(address)"))] = true;
criticalSelectors[bytes4(keccak256("_setPriceOracle(address)"))] = true;
criticalSelectors[bytes4(keccak256("transferOwnership(address)"))] = true;
criticalSelectors[bytes4(keccak256("upgradeTo(address)"))] = true;
}
function _quorumReached(uint256 proposalId)
internal view override returns (bool)
{
if (_containsCriticalAction(proposalId)) {
// Critical actions need 80% approval, not 50%
uint256 forVotes = proposalForVotes(proposalId);
uint256 totalVotes = forVotes + proposalAgainstVotes(proposalId);
return totalVotes >= quorum() &&
forVotes * 100 / totalVotes >= 80;
}
return super._quorumReached(proposalId);
}
function _containsCriticalAction(uint256 proposalId)
internal view returns (bool)
{
(address[] memory targets, , bytes[] memory calldatas, ) =
proposals[proposalId].getActions();
for (uint256 i = 0; i < calldatas.length; i++) {
if (calldatas[i].length >= 4) {
bytes4 selector = bytes4(calldatas[i][:4]);
if (criticalSelectors[selector]) return true;
}
}
return false;
}
}
What this prevents: Payload obfuscation attacks (Vector 3). Even if token holders don't read the calldata, the contract itself enforces higher thresholds for dangerous operations.
Pattern 5: Adaptive Quorum with Participation Weighting
Dynamic quorum that increases when voter participation is abnormally low.
contract AdaptiveQuorumGovernor is Governor {
uint256 public constant BASE_QUORUM_BPS = 1000; // 10%
uint256 public constant MAX_QUORUM_BPS = 4000; // 40%
uint256 public avgParticipationBps;
function quorum(uint256 timepoint) public view override returns (uint256) {
uint256 totalSupply = token.getPastTotalSupply(timepoint);
uint256 adjustedQuorum = BASE_QUORUM_BPS;
if (avgParticipationBps > 0) {
adjustedQuorum = BASE_QUORUM_BPS * avgParticipationBps /
_currentParticipationBps();
if (adjustedQuorum > MAX_QUORUM_BPS) {
adjustedQuorum = MAX_QUORUM_BPS;
}
}
return (totalSupply * adjustedQuorum) / 10000;
}
}
What this prevents: Low-quorum treasury drains (Vector 2). When participation drops, quorum rises automatically.
Pattern 6: Time-Weighted Voting Power
Weight voting power by holding duration. Recent purchasers get reduced influence.
function getVotingPower(address account, uint256 proposalBlock)
public view returns (uint256)
{
uint256 balance = token.getPastVotes(account, proposalBlock);
uint256 holdingDuration = _getHoldingDuration(account, proposalBlock);
// Linear ramp: 0% power at 0 days, 100% at 30 days
uint256 multiplier;
if (holdingDuration >= 30 days) {
multiplier = 10000; // 100%
} else {
multiplier = (holdingDuration * 10000) / 30 days;
}
return (balance * multiplier) / 10000;
}
What this prevents: Market purchase attacks where tokens are bought shortly before a proposal snapshot.
Pattern 7: Optimistic Governance with Challenge Period
Proposals are assumed to pass unless challenged. Challenges trigger full voting.
contract OptimisticGovernor {
uint256 public constant CHALLENGE_PERIOD = 5 days;
uint256 public constant CHALLENGE_BOND = 10000e18;
enum ProposalState { Proposed, Challenged, Executed, Cancelled }
function challenge(uint256 proposalId) external {
require(proposals[proposalId].state == ProposalState.Proposed);
require(block.timestamp < proposals[proposalId].deadline);
token.transferFrom(msg.sender, address(this), CHALLENGE_BOND);
proposals[proposalId].state = ProposalState.Challenged;
_startFullVote(proposalId);
}
}
What this prevents: Reduces attack surface by requiring active monitoring. Routine proposals execute faster, suspicious ones get escalated.
Implementation Checklist: Defense-in-Depth
Minimum viable defense (all protocols):
- ✅ Snapshot-based voting (Pattern 1) — prevents flash loans
- ✅ Break Glass Guardian (Pattern 3) — emergency backstop
- ✅ Proposal validation hooks (Pattern 4) — auto-detect critical actions
Enhanced defense (>$10M TVL):
- ✅ Vote escrow with lock period (Pattern 2)
- ✅ Adaptive quorum (Pattern 5)
Maximum security (>$100M TVL):
- ✅ Time-weighted voting (Pattern 6)
- ✅ Optimistic governance (Pattern 7)
Monitoring (all protocols):
- Alert on large token transfers to new addresses
- Monitor proposal creation from recent token acquirers
- Track delegation concentration changes
- Alert on proposals targeting admin/upgrade/oracle functions
The Solana Parallel: SPL Governance Risks
Solana's governance tooling (SPL Governance, Realms) faces the same attack classes with Solana-specific twists:
// Solana SPL Governance: Ensure voting weight uses token lock, not balance
pub fn get_voter_weight(
governing_token_deposit: &GoverningTokenDeposit,
) -> Result<u64> {
let deposit_time = governing_token_deposit.deposit_timestamp;
let current_time = Clock::get()?.unix_timestamp;
let holding_duration = current_time - deposit_time;
// Require minimum 3-day deposit before voting weight activates
if holding_duration < 3 * 86400 {
return Ok(0);
}
Ok(governing_token_deposit.amount)
}
Key differences on Solana:
- No flash loans in the EVM sense, but atomic CPI chains can achieve similar effects
- SPL Governance uses explicit deposit/withdrawal of tokens into governance accounts
- Realms voter weight plugins allow custom weight calculations — use them for time-weighting
- Account close attacks can disrupt governance records if not properly handled
The Cost of Doing Nothing
The governance attack surface is expanding because:
- More DAOs are being created — Aragon, Tally, and Realms make DAO deployment trivial, but don't enforce security best practices
- Token concentration is increasing — Low-float tokens on secondary chains make quorum-level purchases affordable
- Cross-chain governance is emerging — Multi-chain voting adds bridge delays, finality gaps, and synchronization vulnerabilities
- AI-assisted attacks are coming — LLMs can scan proposal thresholds, model attack economics, and identify vulnerable DAOs automatically
The Moonwell attack cost $1,800. The next one might cost $18. If your protocol uses on-chain governance without at minimum snapshot voting, vote escrow, and an emergency guardian, you're not "decentralized" — you're just undefended.
This analysis is part of the DeFi Security Research series. All code examples are for educational and defensive purposes. Previous entries covered the Aave V3 fork epidemic, the zero-cost Solana security pipeline, and custom detector development.
Top comments (0)