We develop on-chain voting systems for DAOs of any scale—from small communities to protocols with billion-dollar capitalization. Our stack: Solidity 0.8.x, Foundry, OpenZeppelin Governor. You get a reliable governance system: token with snapshot voting, Timelock, flash loan attack protection, and customization to fit your management model.
The main problem of most DAOs is either overly centralized design (everything decided by multisig) or dysfunctional (quorum never met). We find the balance: we tune voting delay, quorum, and timelock based on analysis of your community. 10+ years of blockchain development experience, 30+ implemented DAO projects—we guarantee a working result. According to DefiLlama, more than 50% of DAOs lack protective Timelock, which is critical for security.
OpenZeppelin Governor is 4x more flexible than Compound Governor Bravo thanks to modular mixins, and Foundry for testing accelerates the development cycle by 3x compared to Hardhat. You save up to 40% on audits due to built-in formal checks at the testing stage.
What Problems Do We Solve?
Suboptimal quorum—too low lets malicious proposals pass, too high paralyzes governance. We analyze real voter turnout and calibrate quorum manually. Flash loan governance attacks—an attacker takes a flash loan, gets huge voting power, passes a proposal, and drains the treasury. Our defense: voting delay + snapshot-based voting. Centralization via multisig—if all key decisions go through a 3/5 multisig, it is not a DAO. We build fully on-chain governance with gradual decentralization. Loss of funds due to Timelock errors—leaving an admin role in TimelockController is a cause of many hacks. We automatically revoke all admin roles after deployment.
Architecture: Token, Governor, and Timelock
The basic set of contracts for a DAO—Governance Token (ERC-20 with Votes), Governor (voting core), and TimelockController (protective delay).
How to Set Up OpenZeppelin Governor?
Minimal assembly through inheritance of mixins:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/governance/Governor.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol";
import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol";
contract MyDAO is
Governor,
GovernorSettings,
GovernorCountingSimple,
GovernorVotes,
GovernorVotesQuorumFraction,
GovernorTimelockControl
{
constructor(
IVotes _token,
TimelockController _timelock
)
Governor("MyDAO")
GovernorSettings(
1 days, // voting delay
1 weeks, // voting period
100_000e18 // proposal threshold
)
GovernorVotes(_token)
GovernorVotesQuorumFraction(4) // 4% quorum
GovernorTimelockControl(_timelock)
{}
// Overrides required to resolve mixin conflicts
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 blockNumber)
public view override(Governor, GovernorVotesQuorumFraction)
returns (uint256) { return super.quorum(blockNumber); }
function state(uint256 proposalId)
public view override(Governor, GovernorTimelockControl)
returns (ProposalState) { return super.state(proposalId); }
function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values,
bytes[] memory calldatas, bytes32 descriptionHash)
internal override(Governor, GovernorTimelockControl) {
super._execute(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(); }
function supportsInterface(bytes4 interfaceId)
public view override(Governor, GovernorTimelockControl) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
Why is Timelock Critically Important?
TimelockController is the delay between proposal acceptance and execution. Without it, an attacker who gains control over voting can instantly drain the entire treasury.
// Deploy TimelockController
TimelockController timelock = new TimelockController(
2 days, // minDelay
proposers, // who can queue (Governor)
executors, // who can execute (address(0) = anyone)
admin // admin (usually address(0) after setup)
);
// Assign roles
timelock.grantRole(timelock.PROPOSER_ROLE(), address(governor));
timelock.grantRole(timelock.CANCELLER_ROLE(), address(governor));
timelock.grantRole(timelock.EXECUTOR_ROLE(), address(0));
// Critical: revoke admin from deployer!
timelock.revokeRole(timelock.TIMELOCK_ADMIN_ROLE(), deployer);
The last step is often skipped—resulting in the deployer being able to bypass governance. We always verify this.
Governance Token with ERC-20 Votes
The voting token must implement the IVotes interface. OpenZeppelin ERC20Votes stores checkpoint history of balances for snapshot-based voting.
contract GovernanceToken is ERC20, ERC20Permit, ERC20Votes {
constructor(address initialHolder)
ERC20("MyDAO Token", "MDT")
ERC20Permit("MyDAO Token")
{
_mint(initialHolder, 10_000_000e18);
}
function _afterTokenTransfer(address from, address to, uint256 amount)
internal override(ERC20, ERC20Votes) {
super._afterTokenTransfer(from, to, amount);
}
function _mint(address to, uint256 amount)
internal override(ERC20, ERC20Votes) {
super._mint(to, amount);
}
function _burn(address account, uint256 amount)
internal override(ERC20, ERC20Votes) {
super._burn(account, amount);
}
}
An important nuance: in ERC20Votes, tokens have no voting power until the owner calls delegate(address). We automate self-delegation at first transfer to avoid confusion.
Proposal Lifecycle and Voting with Rationale
A proposal goes through stages: Pending → Active → Succeeded/Defeated → Queued → Executed (or Canceled). Each vote can be accompanied by rationale—this increases transparency. We implement hybrid voting: gasless voting via EIP-712 signatures (off-chain vote collection with on-chain finalization) and traditional on-chain voting. The relayer pays gas, the user signs the vote off-chain—lowering participation barriers.
Treasury Management and Flash Loan Attack Protection
The treasury contract is controlled by the Governor through Timelock. Additionally, a Guardian multisig (e.g., 5/9) is set for emergency pause—it can only freeze funds, not spend them.
Flash loan protection: voting delay (minimum 1 day) prevents an attacker from voting instantly. The snapshot fixes balances at the block of proposal creation, not at voting time.
Custom Mechanics and Upgrade
We add Quadratic voting (voting power = sqrt(balance)) to reduce whale influence and Conviction voting for continuous funding. Governor contracts are deployed via UUPS proxy—upgrades go through a full governance cycle via self-call.
function _getVotes(
address account,
uint256 blockNumber,
bytes memory /*params*/
) internal view virtual override returns (uint256) {
uint256 balance = token.getPastVotes(account, blockNumber);
return _sqrt(balance);
}
function _sqrt(uint256 x) internal pure returns (uint256 y) {
if (x == 0) return 0;
uint256 z = (x + 1) / 2;
y = x;
while (z < y) {
y = z;
z = (x / z + z) / 2;
}
}
contract UpgradeableGovernor is Governor, UUPSUpgradeable {
function _authorizeUpgrade(address newImplementation)
internal override onlyGovernance {}
modifier onlyGovernance() {
require(msg.sender == address(this), "Only governance can upgrade");
_;
}
}
During UUPS upgrade, the logic updates in the implementation while storage stays in the proxy. All changes go through the voting mechanism: a proposal is created with a call to _authorizeUpgrade, voting, Timelock, then execution. This guarantees decentralized upgrade management.
Common Mistakes and Recommended Parameters
- Short Timelock. 24 hours is too little for DeFi. Set 48–72 hours, for major upgrades—7 days.
- Low quorum. 4% is normal for large protocols, but for a small community real activity may be lower. Calibrate after launch.
- Missing proposal threshold. Without a threshold, anyone can spam proposals. Set threshold = 0.5–1% of total supply.
- Leftover admin role. Always revoke TIMELOCK_ADMIN_ROLE from the deployer.
| Parameter | Small DAO | DeFi Protocol | Treasury DAO |
|---|---|---|---|
| Voting Delay | 1 day | 2 days | 1 day |
| Voting Period | 5 days | 7 days | 7 days |
| Timelock | 24 h | 72 h | 48 h |
| Quorum | 10% | 4% | 5% |
| Proposal Threshold | 1% | 0.25% | 0.5% |
| Mechanic | OpenZeppelin Governor | Compound Governor Bravo |
|---|---|---|
| Modularity | Yes (mixins) | No (fixed logic) |
| Timelock | Built-in support | Requires external contract |
| Upgrade | Via UUPS | Via delegatecall |
| Gas efficiency | Higher (optimized storage) | Lower |
What Is Included and Timelines
We provide:
- Design of voting mechanics (choice of voting model, quorum, timelock)
- Smart contracts (ERC-20 with Votes token, Governor, Timelock, treasury)
- Full test suite (mainnet fork tests, attack simulations)
- Deployment and configuration (including admin role revocation)
- Documentation and team training
- Support for the first 3 months after launch
Timelines—from 4 weeks for a basic system to 16 weeks with customizations and frontend. Cost varies, but on average 30% lower than analogs with similar functionality. Contact us to discuss your project and get a preliminary estimate. We guarantee transparency at all stages—from design to deployment. Request a free consultation.







