A group of people pool money for a shared purpose, a club, an open-source project, a student association, a co-working space, and need to decide, together, how to spend it. Today, someone holds the bank account and the vote is advisory. Build the version where the vote is the spending: money leaves the treasury only when a proposal passes under rules that were public before it was made, and nobody has a key to the account that bypasses this.
Why this needs a chain
The treasury is a contract. It holds funds. It has no owner. The only way funds leave is the execution of a proposal that has passed a vote, and anyone can trigger that execution once the conditions are met. Members can read the rules, who can vote, how much a vote weighs, what quorum is required, how long voting lasts, how long the delay before execution is, and those rules apply to every proposal, including the ones to change the rules. A treasurer who disagrees with the outcome has no override, because there is no treasurer.
What is still trusted: whoever decides who is a member. Make that explicit. A membership controlled by a single admin makes the admin the treasurer by another name; a membership controlled by the members through the same proposal mechanism closes the loop, and has its own failure modes, a majority can vote to expel a minority. Your roles table has to pick a position and defend it.
Actors
| Role | Does |
|---|---|
| Member | Holds voting power, creates proposals, votes |
| Anyone | Sends funds to the treasury, executes passed proposals after the delay |
| Proposal target | The address that receives funds or gets called when a proposal executes |
| Deployer | Sets the initial rules and members, and then, ideally, has no further power |
Voting power can be one-member-one-vote (a membership NFT or an allowlist) or token-weighted (an ERC-20 where balance is weight). Both are fine. Both have different attacks. Choose one and say why.
Core features
- A treasury that receives native ETH from anyone. Receiving ERC-20 is an extension.
- Membership. A way to know who can vote and with what weight, and a way for that set to change that goes through governance itself, not through an admin.
- Proposals. A member creates a proposal: a recipient, an amount, and a description (or its hash). A proposal has a lifecycle, pending, active, passed or failed, queued, executed, cancelled, and each transition is an event.
- Voting. During a voting window, members vote for or against. A vote is counted once per member per proposal. The tally is readable by anyone at any time.
- Quorum and threshold. A proposal passes only if enough voting power participated and enough of it was in favour. Both parameters are public and are set at deployment.
- Timelock. A passed proposal cannot execute immediately. A delay gives members who disagree time to react, to leave, to raise the alarm, to propose a cancellation. The delay is a rule, not a courtesy.
- Execution by anyone. Once the delay has passed, any address can call
executeand the treasury pays out. Execution is not a privilege. - Changing the rules (quorum, threshold, voting period, delay) is itself a proposal, subject to the same process.
Extensions
- Delegation: a member lends their voting power to another address, and can take it back.
- Vote by signature (EIP-712): members sign a vote off chain and anyone submits it, so voting costs the voter no gas.
- Snapshot voting power at proposal creation, so buying or borrowing tokens after a proposal is made does not change its outcome, and explain the attack this prevents.
- Ragequit: a member who disagrees with a passed proposal can leave before it executes and take their share of the treasury with them.
- Proposals that call arbitrary contracts rather than only transferring funds, which turns the treasury into a general-purpose DAO, and enlarges the attack surface accordingly.
- An indexer that presents proposal history, participation rates, and each member’s voting record.
Traps
- An admin backdoor. If the deployer keeps a function that moves funds or changes members “for emergencies”, the treasury has a treasurer. If you genuinely want an emergency mechanism, make it a multi-signature of members, with a timelock, and put it in the roles table in bold.
- Double voting through transfers: with token-weighted voting and no snapshot, a member votes, transfers the tokens to a second address, and votes again. Snapshot, or lock tokens during voting.
- Quorum that can never be met once members drift away, freezing the treasury forever. Think about what happens when participation falls.
- Proposals that execute anything, from a governance that anyone can join. A treasury that runs arbitrary calls with an open membership is a faucet. Constrain one or the other.
- Counting the tally in the frontend. The contract decides whether a
proposal passed. The frontend displays it. If your
executefunction trusts a boolean from the caller, the vote is theatre. - Reentrancy on execution. The recipient of a payout is an arbitrary address, and it may be a contract. Mark the proposal executed before paying.
What the demo should show
Three browser profiles as members, on Base Sepolia. One creates a proposal to pay a fourth address. Two vote in favour, one against; the tally updates on screen and on the explorer. The voting period ends, you will have deployed with a short period for the demo, and say so, the proposal is queued, the delay passes, and a non-member profile executes it. The recipient’s balance changes. Then the failures: a non-member tries to vote and is reverted; a member tries to vote twice and is reverted; someone tries to execute before the delay and is reverted, each with a readable reason in the UI. Finally, the rules change: show a proposal that changed the quorum, already executed, and the new quorum in effect.