A large token holder participating in Uniswap, Compound, Aave, and other DAOs faces different Governor interfaces, proposal logic, and deadlines daily. Missing a deadline is common. Our aggregator provides a unified dashboard, monitoring, delegation, and automatic execution of votes. Our team has 5+ years of experience and has delivered over 20 smart contracts. Contact us for an architecture consultation tailored to your stack.
Architecture system
The aggregator consists of three layers: Indexing layer — indexes events from Governor contracts via adapters (each protocol has its own specifics — Governor Bravo vs OZ Governor differ). State layer — a uniform data model: proposals normalized into {id, protocol, status, deadline, description, calldata}. Action layer — on-chain or off-chain mechanism for executing votes.
How do protocol adapters work?
Each Governor-compatible contract has a slightly different ABI. OZ Governor v4/v5 differs from Compound Governor Bravo, which differs from Compound Governor Alpha. For each we write an adapter implementing a unified interface:
interface GovernorAdapter {
getProposals(fromBlock: number): Promise<Proposal[]>;
getProposalState(proposalId: bigint): Promise<ProposalState>;
castVote(proposalId: bigint, support: number): Promise<TransactionRequest>;
getVotingPower(voter: string, blockNumber: number): Promise<bigint>;
}
class OZGovernorAdapter implements GovernorAdapter {
constructor(private contract: Contract) {}
async getProposals(fromBlock: number) {
const filter = this.contract.filters.ProposalCreated();
const events = await this.contract.queryFilter(filter, fromBlock);
return events.map(e => this.normalizeProposal(e));
}
async castVote(proposalId: bigint, support: number) {
return {
to: this.contract.target,
data: this.contract.interface.encodeFunctionData('castVote', [proposalId, support])
};
}
}
class CompoundBravoAdapter implements GovernorAdapter {
async castVote(proposalId: bigint, support: number) {
return {
to: this.contract.target,
data: this.contract.interface.encodeFunctionData('castVote', [proposalId, support])
};
}
}
During development, check ready-made adapters in libraries like wagmi/viem or The Graph subgraphs.
On-chain component: batch vote
The aggregator contract allows voting in multiple DAOs with one transaction using the Multicall pattern. The contract code is minimal and auditable.
contract GovernanceAggregator {
struct VoteInstruction {
address governor;
uint256 proposalId;
uint8 support;
bytes reason;
}
mapping(address => mapping(address => bool)) public authorizedDelegates;
modifier onlyAuthorized(address voter) {
require(
msg.sender == voter || authorizedDelegates[voter][msg.sender],
"Not authorized"
);
_;
}
function batchVote(
address voter,
VoteInstruction[] calldata instructions
) external onlyAuthorized(voter) {
for (uint i = 0; i < instructions.length; i++) {
VoteInstruction calldata inst = instructions[i];
try IGovernor(inst.governor).castVoteWithReason(
inst.proposalId,
inst.support,
string(inst.reason)
) {
emit VoteCast(voter, inst.governor, inst.proposalId, inst.support);
} catch Error(string memory reason) {
emit VoteFailed(voter, inst.governor, inst.proposalId, reason);
}
}
}
}
An important nuance: each Governor checks msg.sender as the voter. The aggregator votes from its own address — this works only if voting power is delegated to the aggregator address. An alternative approach is using a smart wallet with DELEGATECALL, but that adds security complexity.
Delegation and proportional voting
For ERC-20Votes tokens, the user delegates voting power to the aggregator address. The problem: if 60% want For and 40% Against, the aggregator should vote proportionally. The solution is fractional voting: the aggregator votes with its weight proportionally. Compound Governor Bravo supports castVoteWithWeightBySig, and OZ Governor v5 added GovernorCountingFractional.
Automatic voting rules
A key feature is automatic execution of votes based on predefined rules. Example: always vote Against for treasury proposals > $1M. An off-chain service analyzes new proposals, extracts parameters from calldata, and applies rules. If conditions are not met, a default vote is used.
interface VotingRule {
protocol: string;
proposalType: string;
conditions: Condition[];
defaultVote: 0 | 1 | 2;
requireConfirmation: boolean;
}
Parsing proposal calldata
We need to extract meaning from calldata. For example, a Compound proposal to change collateral factor:
const KNOWN_SIGNATURES = {
'0x3c3e4f7a': { name: 'setCollateralFactor', protocol: 'compound' },
'0x5b85a600': { name: '_setReserveFactor', protocol: 'compound' },
};
function parseProposalCalldata(calldata: string): ProposalAction {
const selector = calldata.slice(0, 10);
const known = KNOWN_SIGNATURES[selector];
if (!known) return { type: 'unknown', selector };
const iface = new ethers.Interface([`function ${known.name}(...)`]);
const decoded = iface.decodeFunctionData(known.name, calldata);
return { type: known.name, protocol: known.protocol, params: decoded };
}
This is labor-intensive work for each protocol.
Notifications and deadlines
Notification system: poll active proposals every N minutes or subscribe via WebSocket (eth_subscribe logs). Notifications via email / Telegram / webhook on proposal creation, approaching deadline (24 hours before), and execution. Deadline calculation: deadlineBlock * avgBlockTime + referenceTimestamp, accuracy ±5 minutes.
Development stack
Backend: Node.js + TypeScript + viem + BullMQ + PostgreSQL. Indexing: The Graph subgraphs + custom listener. Frontend: React + wagmi + Radix UI. Smart contract: Solidity 0.8.x + Foundry.
What's included in the work
| Component | Description |
|---|---|
| Protocol analysis | Identify supported DAOs and their Governor contracts |
| Adapter development | Create normalizers for each protocol |
| Aggregator smart contract | Implement batch vote with try/catch |
| Backend for indexing and rules | Monitoring, parsing, automatic voting |
| Frontend dashboard | View proposals and manage rules |
| Notifications | Telegram/Email alerts |
| Security audit | Check contract and backend |
| Documentation | API, rules, architecture |
Comparison with manual voting
The aggregator reduces monitoring time by 3x. Instead of 5–10 minutes to check all active proposals, it takes 1 minute on the dashboard. Automatic rules eliminate human error. Contact us for a project estimate based on the number of protocols.
Why our approach is more reliable?
Every contract is checked for reentrancy, overflow, and uses formal verification with Echidna (fuzzing). Our team has 5+ years in blockchain development. The aggregator does not store user funds; all transactions are signed by an authorized delegate.
Indicative timelines
| Feature | Timeline |
|---|---|
| Basic indexer (3–5 protocols) | 2–3 weeks |
| On-chain batch vote contract | 1 week |
| Rules and automation | 2–3 weeks |
| Frontend dashboard | 2–3 weeks |
| Notifications | 1 week |
MVP with manual batch voting takes 4–5 weeks. Full aggregator takes 2–3 months.
Contact us — we'll calculate cost and timelines for your specific protocols.







