skills/protocol-patterns/dao-governance/SKILL.md
Governance security patterns for voting, timelocks, proposal execution, and quorum safety.
npx skillsauth add apegurus/solidity-argus dao-governanceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Governance protocols enable token-based decision making. Core security concerns: voting manipulation, flash loan attacks, proposal execution, and timelock bypasses.
┌─────────────────────────────────────────────────────────────┐
│ GOVERNANCE SYSTEM │
├─────────────────────────────────────────────────────────────┤
│ │
│ PROPOSE → VOTE → EXECUTE │
│ ──────── ──── ─────── │
│ Create proposal Cast votes After timelock │
│ Meet threshold Snapshot power Run actions │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ TIMELOCK │ │
│ │ Queue → Wait (e.g., 2 days) → Execute │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Attack Vectors:
Checklist:
// VULNERABLE: Current balance for voting
function vote(uint256 proposalId, bool support) external {
uint256 votes = token.balanceOf(msg.sender); // Flashloanable!
_castVote(proposalId, msg.sender, support, votes);
}
// SECURE: Historical balance (ERC20Votes)
function vote(uint256 proposalId, bool support) external {
uint256 votes = token.getPastVotes(
msg.sender,
proposals[proposalId].snapshot // Block when proposal created
);
_castVote(proposalId, msg.sender, support, votes);
}
Attack Vectors:
Checklist:
// VULNERABLE: No execution state check
function execute(uint256 proposalId) external {
Proposal storage p = proposals[proposalId];
require(p.state == ProposalState.Succeeded, "Not succeeded");
// Missing: p.state = ProposalState.Executed;
for (uint256 i; i < p.targets.length; i++) {
p.targets[i].call(p.calldatas[i]);
}
// Can be called again!
}
Attack Vectors:
Checklist:
// VULNERABLE: Quorum based on current supply
function quorum() public view returns (uint256) {
return token.totalSupply() * 4 / 100; // Can be manipulated
}
// SECURE: Quorum at snapshot
function quorum(uint256 blockNumber) public view returns (uint256) {
return token.getPastTotalSupply(blockNumber) * 4 / 100;
}
Attack Vectors:
Checklist:
uint256 public constant MINIMUM_DELAY = 2 days;
uint256 public constant MAXIMUM_DELAY = 30 days;
uint256 public constant GRACE_PERIOD = 14 days;
function setDelay(uint256 delay) external {
require(msg.sender == address(this), "Only self");
require(delay >= MINIMUM_DELAY, "Too short");
require(delay <= MAXIMUM_DELAY, "Too long");
delay_ = delay;
}
Attack Vectors:
Checklist:
// Attack flow:
// 1. Flash loan governance tokens
// 2. Create proposal (if allowed in same tx)
// 3. Vote with flash-loaned tokens
// 4. Execute proposal immediately (no timelock)
// 5. Proposal drains funds
// 6. Return flash loan
// ALL of these should fail:
// - Snapshot should be from past block
// - Voting delay should prevent same-block voting
// - Timelock should delay execution
// VULNERABLE: Proposal can be front-run
function propose(
address[] memory targets,
bytes[] memory calldatas,
string memory description
) public returns (uint256) {
uint256 proposalId = hashProposal(targets, calldatas, keccak256(bytes(description)));
// Attacker can front-run with same proposal, different description
}
// VULNERABLE: delegatecall in governor
function execute(...) external {
for (uint i; i < targets.length; i++) {
(bool success,) = targets[i].delegatecall(calldatas[i]);
// delegatecall in governance = CRITICAL RISK
}
}
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
contract SecureGovernor is Governor, GovernorVotes, GovernorTimelockControl {
function votingDelay() public pure override returns (uint256) {
return 1 days; // Time before voting starts
}
function votingPeriod() public pure override returns (uint256) {
return 1 weeks; // Voting duration
}
}
// Lock tokens for voting power (Curve model)
// Longer lock = more voting power
// Prevents flash loan attacks by requiring locked tokens
struct LockedBalance {
uint256 amount;
uint256 unlockTime;
}
function votingPower(address user) public view returns (uint256) {
LockedBalance memory lock = locked[user];
if (lock.unlockTime <= block.timestamp) return 0;
uint256 timeLeft = lock.unlockTime - block.timestamp;
return lock.amount * timeLeft / MAX_LOCK_TIME;
}
testing
Specialist profile for mechanically applying the attack-vector deck and classifying vectors as skip, drop, or investigate.
tools
Specialist profile for libraries, helpers, base contracts, adapters, encoders, wrappers, and integration glue.
testing
Specialist profile for rounding, scale, decimal, downcast, and arithmetic accounting edge cases.
testing
Specialist profile for extracting conservation laws and state couplings, then searching for violating paths.