Voting Strategies Standard
Modular voting strategy contracts for Lux DAO governance
Abstract
This LP specifies the voting strategy contracts used by Lux DAOs. Strategies define how votes are weighted, who can propose, and how voting power is calculated. The modular design allows DAOs to choose or create custom voting mechanisms.
Motivation
Modular voting strategies enable:
- Flexibility: Different voting mechanisms per DAO
- Fairness: Quadratic voting to reduce plutocracy
- Composability: Combine multiple token types
- Customization: DAO-specific voting rules
Specification
Strategy Interface
// Base interface for all voting strategies
interface IBaseStrategy {
function initializeProposal(bytes memory _data) external;
function isPassed(uint32 _proposalId) external view returns (bool);
function isProposer(address _address) external view returns (bool);
function votingEndBlock(uint32 _proposalId) external view returns (uint256);
function getProposalVotes(uint32 _proposalId) external view returns (
uint256 noVotes,
uint256 yesVotes,
uint256 abstainVotes
);
}
Available Strategies
1. VotingWeightERC20V1
Token-weighted voting (1 token = 1 vote).
// Location: contracts/contracts/strategies/voting-weight/VotingWeightERC20V1.sol
interface IVotingWeightERC20V1 {
function governanceToken() external view returns (address);
function quorumNumerator() external view returns (uint256);
function quorumDenominator() external view returns (uint256);
function votingPeriod() external view returns (uint256);
function getVotingWeight(address _voter, uint32 _proposalId) external view returns (uint256);
}
Configuration:
| Parameter | Type | Description |
|---|---|---|
governanceToken | address | ERC20 voting token |
quorumNumerator | uint256 | Quorum numerator (e.g., 400 = 4%) |
quorumDenominator | uint256 | Quorum denominator (10000) |
votingPeriod | uint256 | Voting duration in blocks |
proposerThreshold | uint256 | Min tokens to propose |
2. VotingWeightERC721V1
NFT-weighted voting (1 NFT = 1 vote).
// Location: contracts/contracts/strategies/voting-weight/VotingWeightERC721V1.sol
interface IVotingWeightERC721V1 {
function governanceToken() external view returns (address);
function getVotingWeight(address _voter, uint32 _proposalId) external view returns (uint256);
}
3. Vote Trackers
Track votes for different token types:
// ERC20 vote tracking
// Location: contracts/contracts/strategies/vote-trackers/VoteTrackerERC20V1.sol
interface IVoteTrackerERC20V1 {
function trackVote(address _voter, uint32 _proposalId, uint8 _voteType) external;
function getVoteWeight(address _voter, uint32 _proposalId) external view returns (uint256);
}
// ERC721 vote tracking
// Location: contracts/contracts/strategies/vote-trackers/VoteTrackerERC721V1.sol
interface IVoteTrackerERC721V1 {
function trackVote(address _voter, uint32 _proposalId, uint8 _voteType, uint256[] calldata _tokenIds) external;
}
4. Proposer Adapters
Control who can create proposals:
// ERC20 proposer adapter
// Location: contracts/contracts/strategies/proposer-adapters/ProposerAdapterERC20V1.sol
interface IProposerAdapterERC20V1 {
function proposerThreshold() external view returns (uint256);
function isProposer(address _address) external view returns (bool);
}
// ERC721 proposer adapter
// Location: contracts/contracts/strategies/proposer-adapters/ProposerAdapterERC721V1.sol
interface IProposerAdapterERC721V1 {
function minTokensToPropose() external view returns (uint256);
function isProposer(address _address) external view returns (bool);
}
// Hats proposer adapter (role-based)
// Location: contracts/contracts/strategies/proposer-adapters/ProposerAdapterHatsV1.sol
interface IProposerAdapterHatsV1 {
function proposerHatId() external view returns (uint256);
function isProposer(address _address) external view returns (bool);
}
File Structure
contracts/contracts/strategies/
├── StrategyV1.sol # Base strategy
├── voting-weight/
│ ├── VotingWeightERC20V1.sol # ERC20 weighted
│ └── VotingWeightERC721V1.sol # ERC721 weighted
├── vote-trackers/
│ ├── VoteTrackerERC20V1.sol # ERC20 tracking
│ └── VoteTrackerERC721V1.sol # ERC721 tracking
└── proposer-adapters/
├── ProposerAdapterERC20V1.sol # Token threshold
├── ProposerAdapterERC721V1.sol # NFT threshold
└── ProposerAdapterHatsV1.sol # Role-based (Hats)
Voting Token Contracts
// Standard ERC20 with voting
// Location: contracts/contracts/erc20/VotesERC20V1.sol
interface IVotesERC20V1 {
function delegate(address delegatee) external;
function delegates(address account) external view returns (address);
function getVotes(address account) external view returns (uint256);
function getPastVotes(address account, uint256 blockNumber) external view returns (uint256);
}
// Staked ERC20 with voting
// Location: contracts/contracts/erc20/VotesERC20StakedV1.sol
interface IVotesERC20StakedV1 {
function stake(uint256 amount) external;
function unstake(uint256 amount) external;
function stakedBalance(address account) external view returns (uint256);
}
Vote Types
enum VoteType {
Against, // 0
For, // 1
Abstain // 2
}
Quorum Calculation
function quorum(uint32 _proposalId) public view returns (uint256) {
uint256 snapshotBlock = proposals[_proposalId].voteStartBlock;
uint256 totalSupply = governanceToken.getPastTotalSupply(snapshotBlock);
return (totalSupply * quorumNumerator) / quorumDenominator;
}
function isPassed(uint32 _proposalId) public view returns (bool) {
(uint256 noVotes, uint256 yesVotes, uint256 abstainVotes) = getProposalVotes(_proposalId);
uint256 totalVotes = noVotes + yesVotes + abstainVotes;
// Check quorum
if (totalVotes < quorum(_proposalId)) return false;
// Simple majority
return yesVotes > noVotes;
}
Usage Example
import { VotingWeightERC20V1__factory } from '@luxdao/sdk'
// Deploy voting strategy
const strategy = await VotingWeightERC20V1__factory.deploy(
governanceToken.address,
400, // 4% quorum
10000, // denominator
50400, // ~7 days voting period
ethers.utils.parseEther('1000') // 1000 tokens to propose
)
// Enable strategy on Azorius
await azorius.enableStrategy(strategy.address)
Rationale
Modular Strategy Pattern
Separating voting strategies from the governance module provides several benefits:
- Upgradability: New strategies can be deployed without modifying core governance
- Flexibility: DAOs can switch strategies via governance proposal
- Audit Efficiency: Each strategy is independently auditable
- Composability: Strategies can be combined or extended
Snapshot-Based Voting
Using block-based snapshots for voting power:
- Flash Loan Protection: Prevents manipulation through borrowed tokens
- Predictable Power: Voting power is fixed at proposal creation
- Delegation Support: Delegated votes captured at snapshot
Proposer Adapters
Separating proposal permissions allows:
- Role-Based Access: Hats Protocol integration for role management
- Token Gates: Different thresholds for different proposal types
- Flexible Permissions: Combine multiple access conditions
Backwards Compatibility
OpenZeppelin Governor Compatibility
The voting strategies maintain compatibility with OpenZeppelin patterns:
- Same vote types (Against, For, Abstain)
- Compatible quorum calculation methods
- Similar delegation interfaces
Compound Governor Compatibility
Maintains familiar patterns from Compound:
- Block-based voting periods
- Proposer threshold requirements
- Quorum percentage model
LP-2801 Azorius Integration
Fully compatible with LP-2521 Azorius module:
- Implements
IBaseStrategyinterface - Works with Azorius proposal lifecycle
- Supports multi-strategy configurations
Test Cases
Strategy Tests
function test_VotingWeightCalculation() public {
// Setup: User has 100 tokens
token.mint(voter, 100 ether);
vm.prank(voter);
strategy.vote(proposalId, uint8(VoteType.For));
(,uint256 yesVotes,) = strategy.getProposalVotes(proposalId);
assertEq(yesVotes, 100 ether);
}
function test_QuorumReached() public {
// Setup: 4% quorum, 10000 total supply
// Need 400 tokens to reach quorum
token.mint(voter, 400 ether);
token.mint(address(1), 9600 ether);
vm.prank(voter);
strategy.vote(proposalId, uint8(VoteType.For));
assertTrue(strategy.isPassed(proposalId));
}
function test_ProposerThreshold() public {
// Need 1000 tokens to propose
token.mint(user, 999 ether);
vm.prank(user);
assertFalse(proposerAdapter.isProposer(user));
token.mint(user, 1 ether);
assertTrue(proposerAdapter.isProposer(user));
}
Delegation Tests
function test_DelegatedVotes() public {
token.mint(delegator, 100 ether);
vm.prank(delegator);
token.delegate(delegatee);
assertEq(token.getVotes(delegatee), 100 ether);
assertEq(token.getVotes(delegator), 0);
}
Security Considerations
- Snapshot Voting: Use block snapshots to prevent flash loan attacks
- Delegation: Implement delegation securely with checkpoints
- Quorum: Set appropriate quorum to prevent minority takeover
- Proposer Threshold: Prevent spam proposals with token threshold
Related LPs
- LP-2521: Azorius Governance Module
- LP-2520: Lux DAO Platform
Copyright
Copyright and related rights waived via CC0.