An on-chain DAO governance dApp — delegate voting power, create proposals, vote For / Against / Abstain, and execute outcomes once quorum is met. Solidity smart contracts with a fast, event-driven ethers.js frontend.
- Token-weighted governance — voting power is tracked by a
GovernanceToken(ERC-20 + delegation), so votes are weighted by delegated balance rather than one-address-one-vote. - Delegation & historical snapshots — holders delegate their voting power; the contract writes per-account vote checkpoints so a proposal's outcome is computed against the supply/voting power snapshotted at proposal time (
getPastVotes/getPastTotalSupply). This prevents flash-loan / last-minute vote buying. - Full proposal lifecycle —
Pending → Active → Succeeded / Defeated → Executed, plusCanceled. State is derived deterministically on-chain from block numbers and tallies. - Quorum enforcement — a proposal must reach 4% of the snapshot total supply and have
For > Againstto succeed. - For / Against / Abstain voting with optional on-chain reason strings, double-vote protection, and per-proposal vote records.
- Polished frontend — MetaMask connection with auto-reconnect, network detection + one-click Sepolia switch, a wallet dropdown (copy address / view on Etherscan / disconnect), live proposal cards with vote bars and quorum progress, toasts, and real-time updates driven by contract events.
| Contract | Responsibility |
|---|---|
GovernanceToken |
ERC-20 with owner-gated minting, delegation, and vote checkpoints. Exposes getVotes, getPastVotes, and getPastTotalSupply (binary-search checkpoint lookup) in the spirit of OpenZeppelin's ERC20Votes / EIP-5805. |
DAO |
The governor. Holds proposals, tallies weighted votes against a snapshot, and enforces the state machine + quorum. Key params: VOTING_DELAY = 1 block, VOTING_PERIOD = 50,400 blocks (~1 week), QUORUM_BPS = 400 (4%). Support values: 0 = Against, 1 = For, 2 = Abstain. |
Voting power is snapshotted at the proposal's creation block, so transferring or delegating tokens after a proposal is created cannot change its outcome.
- ethers.js 5.7 (via CDN) talking to MetaMask (EIP-1193).
- Event-driven UI: subscribes to contract events to update proposal cards and stats in real time, with no page reloads.
- Vanilla JS, no build step — the site is fully static and can be served from any static host.
- Smart contracts: Solidity
^0.8.20 - Tooling / tests: Hardhat 2 +
@nomicfoundation/hardhat-toolbox, Chai, Hardhat Network Helpers - Frontend: ethers.js 5.7, MetaMask, vanilla JS/CSS
- E2E testing: Playwright (Chromium)
- Runtime / package manager: Bun
- CI: GitHub Actions
Add screenshots / a GIF of the running dApp here.
| Proposals view | Wallet connected |
|---|---|
docs/proposals.png |
docs/wallet.png |
This project uses Bun as the runtime and package manager.
bun installbun run compile # hardhat compilebun run test # hardhat testbun run start # serve . -p 3000 -> http://localhost:3000Open the site in a browser with MetaMask installed and switch it to the Sepolia testnet.
Set the required environment variables (e.g. in a .env file or your shell):
export SEPOLIA_RPC_URL="https://sepolia.infura.io/v3/<your-key>"
export PRIVATE_KEY="0x<deployer-private-key>" # never commit thisThen deploy:
bun run deploy:sepolia # hardhat run scripts/deploy.js --network sepoliaThe script deploys GovernanceToken, then DAO wired to it, and prints both addresses. Update CONTRACT_ADDRESS in app.js with the deployed DAO address.
The repo ships with two independent, fully-green test suites that also run in CI on every push and pull request.
36 Hardhat/Chai tests covering:
GovernanceToken: owner-only minting, zero-address guard, delegation assigning voting power, vote-power transfer between delegates,getPastVotescheckpoint behaviour, andgetPastTotalSupply.propose: voting-power gating,ProposalCreatedevent args, stored proposal fields, and the initialPendingstate.castVote: For / Against / Abstain tallies,VoteCastevents, reason strings, double-vote and invalid-support reverts, zero-power reverts, and voting-window enforcement.- State machine & quorum:
Pending → Activetransition,SucceededvsDefeated(including a tie, and a proposal defeated purely on quorum despite more For than Against), andquorumVotes(). execute: success path +ProposalExecuted, reverts for non-succeeded proposals, and no double execution.cancel: proposer-only cancellation (Pending & Active), non-proposer revert, post-window revert, and blocked voting after cancellation.
bun run test5 Playwright tests (Chromium) that serve the static site and verify the UI shell + wallet flow:
- page loads with the DAO Governance title;
- the stats bar renders four cards (Total / Active Proposals, Your Voting Power, Quorum Required);
- tab navigation toggles
aria-selectedacross Active / Pending / Succeeded / Defeated; - with no wallet injected, a "MetaMask not detected" toast appears and the connect button shows No Wallet;
- with a mocked
window.ethereum, clicking Connect transitions to the connected wallet menu (truncated address), the dropdown opens (Copy Address / View on Etherscan / Disconnect), and Disconnect restores the Connect button.
bunx playwright install chromium # one-time
bun run test:e2e # playwright test.github/workflows/ci.yml runs two jobs on every push / PR using Bun:
- contracts —
bunx hardhat compile+bunx hardhat test - e2e — installs Chromium and runs
bunx playwright test
| Network | Contract | Address |
|---|---|---|
| Sepolia | DAO (deployed v1) | 0x92382d1d6d19095217b4de2a8edf452f1e6c55fc |
Note on contract versions. The address currently wired into the live frontend (
CONTRACT_ADDRESSinapp.js) is a simpler v1 membership-style DAO (addMember/createProposal/vote). The contract incontracts/DAO.solis the full, more robust governance implementation — delegation, historical vote checkpoints, weighted For/Against/Abstain voting, and 4% quorum — and is the version exercised by the unit-test suite above. Deploy it withbun run deploy:sepoliaand updateCONTRACT_ADDRESSto point the frontend at the full governor.
.
├── contracts/DAO.sol # GovernanceToken + DAO governor
├── scripts/deploy.js # Deploy GovernanceToken then DAO
├── test/DAO.test.js # 36 Hardhat unit tests
├── e2e/dao.spec.js # 5 Playwright E2E tests
├── hardhat.config.js # solc 0.8.20, optimizer, Sepolia network
├── playwright.config.js # static server + Chromium
├── index.html / app.js / style.css # static frontend
└── .github/workflows/ci.yml # CI: compile + unit tests + E2E