DAO Portal Implementation (Voting, Proposals)
DAO portal is a web interface for managing a decentralized organization: creating proposals, voting with tokens, delegating votes, executing approved decisions via smart contracts.
DAO Architecture
Governance Token (ERC-20 + EIP-2612 for delegation)
|
Governor Contract (OpenZeppelin Governor)
├── Propose (create proposal)
├── CastVote (vote)
├── Queue (queue to Timelock)
└── Execute (execute via Timelock)
|
Timelock Controller
└── Target Contracts (Treasury, Protocol)
Governor Contract
// 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/GovernorTimelockControl.sol";
contract DAOGovernor is
Governor, GovernorSettings, GovernorCountingSimple,
GovernorVotes, GovernorTimelockControl
{
constructor(
IVotes _token,
TimelockController _timelock
)
Governor("DAO Governor")
GovernorSettings(
1 days, // voting delay (1 day before voting starts)
7 days, // voting period (7 days voting)
100_000e18 // proposal threshold (100k tokens required)
)
GovernorVotes(_token)
GovernorTimelockControl(_timelock)
{}
// 4% of tokens required to pass (quorum)
function quorum(uint256 blockNumber) public view override returns (uint256) {
return token.getPastTotalSupply(blockNumber) * 4 / 100;
}
}
Frontend: Proposal Creation
import { useWriteContract, useAccount } from 'wagmi';
import { encodeFunctionData } from 'viem';
function CreateProposal() {
const { writeContractAsync } = useWriteContract();
const handleSubmit = async (formData: ProposalFormData) => {
// Encode target contract call
const calldata = encodeFunctionData({
abi: treasuryAbi,
functionName: 'transfer',
args: [formData.recipient, formData.amount]
});
await writeContractAsync({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'propose',
args: [
[TREASURY_ADDRESS], // targets
[0n], // values (ETH)
[calldata], // calldatas
formData.description // description (Markdown)
]
});
};
return <ProposalForm onSubmit={handleSubmit} />;
}
Frontend: Voting
function VoteOnProposal({ proposalId }) {
const { writeContractAsync } = useWriteContract();
const { address } = useAccount();
// Get voting power at snapshot block
const { data: votingPower } = useReadContract({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'getVotes',
args: [address!, proposalSnapshotBlock]
});
const castVote = async (support: 0 | 1 | 2) => {
// 0 = Against, 1 = For, 2 = Abstain
await writeContractAsync({
address: GOVERNOR_ADDRESS,
abi: governorAbi,
functionName: 'castVoteWithReason',
args: [proposalId, support, reason]
});
};
return (
<div>
<p>Your voting power: {formatTokens(votingPower)} tokens</p>
<button onClick={() => castVote(1)}>For</button>
<button onClick={() => castVote(0)}>Against</button>
<button onClick={() => castVote(2)}>Abstain</button>
</div>
);
}
Indexing via The Graph
// Fetch proposals list
const PROPOSALS_QUERY = gql`
query GetProposals($state: String, $first: Int, $skip: Int) {
proposals(
where: { state: $state }
orderBy: createdAt
orderDirection: desc
first: $first
skip: $skip
) {
id
proposalId
proposer { id }
description
state
forVotes
againstVotes
abstainVotes
quorum
startBlock
endBlock
createdAt
}
}
`;
// Vote delegation
async function delegateVotes(delegatee: string) {
await writeContractAsync({
address: GOVERNANCE_TOKEN,
abi: erc20VotesAbi,
functionName: 'delegate',
args: [delegatee]
});
}
Snapshot Integration (Off-chain Voting)
For signaling votes without gas costs — Snapshot Protocol:
import snapshot from '@snapshot-labs/snapshot.js';
const client = new snapshot.Client712('https://hub.snapshot.org');
// Create vote (signed by wallet, no transaction)
await client.proposal(web3, address, {
space: 'your-dao.eth',
type: 'single-choice',
title: 'Adopt new token strategy?',
body: '## Description\n\nFull proposal description...',
choices: ['For', 'Against', 'Abstain'],
start: Math.floor(Date.now() / 1000),
end: Math.floor(Date.now() / 1000) + 7 * 24 * 3600,
snapshot: await web3.eth.getBlockNumber(),
plugins: JSON.stringify({}),
app: 'your-dao-app'
});
Implementation Timeline
- Governor + Timelock + Governance Token contracts — 2–3 weeks
- Frontend (proposals list, voting, delegation) — 2–3 weeks
- Indexer + full portal — 1–2 months







