Move the proposal body off the pinning vendor and onto Walrus.
Build a Governance contract whose on-chain state is just (proposer, blobId, deadline, yes, no) per
proposal. The proposer pays Walrus for the body upfront; voters read the markdown from the public aggregator and
cast a token-weighted yes/no on chain. Section 4 ships the flash-loan-vulnerability warning that separates the
showcase shape from a production-safe pattern.
What you'll write
One file: src/Governance.sol. The reference-app inlines a minimal IERC20 and uses a
tiny mock vote token in tests so the workspace stays OpenZeppelin-free.
-
s01 · storage + propose
Proposal storage + the propose path
uint256 id = ++lastProposalId;does the sequential-id assignment AND the storage write in one line — pre-increment is what gives proposal #1 the id1rather than0, which matters because the existence check elsewhere isp.deadline != 0. Pre-incrementing also dodges the off-by-one bug where the FIRST proposal would otherwise be indistinguishable from a never-touched slot. -
s02 · vote
Token-weighted voting
voteToken.balanceOf(msg.sender)is the showcase's load-bearing simplification AND its load-bearing security hazard. A livebalanceOfread makes flash loans a vote-override primitive. The lesson holds the simple shape; Section 4 walks through the production fix (ERC20Votes+getPastVotes) and explains whyview/STATICCALL— not statement order — is what protects this call site from reentrancy. -
s03 · tally
The tally view
passed = closed && yes > nois the order that matters.closedfirst — if voting is still open you don't know who wins yet, sopassedmust befalseregardless of the current tally. Choose the encoding that mirrors how the human reading the call thinks about the answer. -
s04 · flash-loan warning
The flash-loan warning — and the production fix
The path from showcase to production is one substitution:
voteToken.balanceOf(msg.sender)→IVotes(voteToken).getPastVotes(msg.sender, p.startBlock). A past-balance snapshot is unforgeable by a flash loan because the snapshot block is in the past — there is no transaction the attacker can issue that retroactively gives them tokens at block N.
Verification
Twelve Foundry tests gate the lesson — propose validation, sequential id + state isolation, weighted vote tallies, double-vote rejection, post-deadline rejection, and the four tally state transitions (open · closed-passed · closed-failed · unknown reverts).
forge test
Sources
Read the reference-app on GitHub, or follow the upstream showcase for the TypeScript propose/tally CLIs.
- Reference-app: lessons/04-dao-proposals/reference-app/ — Foundry workspace, OZ-free, runs offline.
- Lesson sections: lessons/04-dao-proposals/sections/ — the four section prompts.
- evm-sui walkthrough: 04-dao-proposals — DAO governance on Walrus — includes the TypeScript
proposeandtallyCLIs that drive the contract end-to-end. - OpenZeppelin Governor: production governance patterns.