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 the chain is load-bearing
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.