← Contract Hero · WorkLearnPlugins
acc-evm-wal
Lesson 04 · Foundry

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.

Slug: 04-dao-proposals Stack: Foundry · Solidity 0.8.24 · OZ-free (inlined IERC20) Sections: 4 Final gate: forge test · 12 tests

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 id 1 rather than 0, which matters because the existence check elsewhere is p.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 live balanceOf read makes flash loans a vote-override primitive. The lesson holds the simple shape; Section 4 walks through the production fix (ERC20Votes + getPastVotes) and explains why view / STATICCALL — not statement order — is what protects this call site from reentrancy.

  • s03 · tally

    The tally view

    passed = closed && yes > no is the order that matters. closed first — if voting is still open you don't know who wins yet, so passed must be false regardless 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.