// ========================================================================================= // DaoTreasury — a member roll votes on payments of native STR out of this contract, and on // who is on the roll. // // WHAT IT GOVERNS, AND THIS IS THE PART PEOPLE GET WRONG // This contract's own balance. Nothing else. It is NOT SourceLess governance: chain-wide // parameters belong to the Althing, at a strict majority of 66,360 declared power, and the // only governance actions that exist there are set-parameter over a bounded registry, // admit-member and remove-member — deliberately, because a governance system that can // change anything is a governance system that can be used to steal. No contract can touch // any of it. A DAO template that presents itself as SourceLess governance is presenting // authority it does not have, and this one does not. // // WHAT IT GUARANTEES // * The electorate is FROZEN at proposal time. A proposal records the epoch it was created // in and the member count at that instant. Admitting members afterwards does not give // them a vote on it, and removing members afterwards does not shrink the majority it // needs. Both directions are bugs in live-membership tallying: admitting a large member // mid-vote defeats a proposal that had already passed, and removing members shrinks the // threshold until an existing minority's votes clear it. // * One member, one vote, per proposal. A second vote reverts. // * A strict majority of the frozen roll is required: yes * 2 > roll. A tie does not pass. // * A proposal executes at most once, only after it passes, and only before it expires. // Anyone may trigger that execution — it can only do what the frozen roll approved, to the // address the proposal named — so a passed payment cannot stall behind a quiet member. // * There are exactly three kinds of proposal — pay, admit, remove — and no fourth. They are // deliberately the Althing's three: govern the thing you govern, admit a member, remove a // member. Nothing here writes arbitrary state, because a governance system that can change // anything is a governance system that can be used to steal. // * ONCE THE ROLL IS SEALED, ONLY THE ROLL CHANGES THE ROLL. `proposeAdmit` and // `proposeRemove` need the same strict majority a payment needs, and there is no key // anywhere — the retired steward's included — that can put an address on the roll or take // one off it alone. // * A roll of at least `minRollSize`, which ships at three. One member is a majority of one. // * The steward cannot spend, and here is why that is a fact rather than a claim about the // ABI. `execute` can only pay a proposal; `propose` is the only thing that creates one; // and `propose` refuses to run while `stewardActive` is true. So while the steward has any // power over the roll, NO VALUE CAN LEAVE THIS CONTRACT BY ANY PATH — and after // `renounceSteward()`, which is one-way, the steward has no power over the roll at all. // The two states are exclusive by construction, which is the only way this promise can be // kept: see below for what was true before it was. // // THE POWER THE STEWARD KEEPS, DECLARED, AND WHY IT ENDS BEFORE THE MONEY MOVES // The deployer is the steward and can `admit` and `remove` members while the roll is open. // That is real power — it decides who will vote on everything this treasury ever does — and // it is in the ABI as `stewardIs()`, `stewardHasPower()`, `admit` and `remove` so that a // member can see it before joining and a funder can see it before paying in. // // IT IS ALSO, IF IT OUTLIVED THE FUNDING, A CLAIM ON THE WHOLE BALANCE. A steward that can // admit members can admit members IT CONTROLS, and nothing in this contract — nothing in // this chain, from inside a contract — can tell an independent member from an address the // steward holds the key to. So a live steward is, in the limit, a majority of any future // roll. An earlier version of this template said "the steward cannot spend" on the grounds // that no function moving value checks `steward`, and an audit executed the counterexample // on a real chain: remove all five members, admit yourself, propose a payment to yourself, // vote once against a roll of one, execute. Whole treasury, four calls, chain still valid. // The self-admit ban and the roll floor below make that particular sequence revert; they do // not, and cannot, stop a steward that simply admits four addresses it owns. // // The template therefore separates the two powers in time instead of trying to bound one of // them, and hands the roll to the roll. Build the roll, then `renounceSteward()`, then fund // it. After renunciation `stewardHasPower()` reads false, the steward has no function it can // call, and the membership at that instant is readable in full from the `Admitted` and // `Removed` log — a set decided before any money was at stake, that a funder can inspect // before paying in. DO NOT FUND A TREASURY WHOSE `stewardHasPower()` IS STILL TRUE. It // cannot spend, but it can still decide, alone, who will. // // AFTER THAT, THE ROLL IS NOT FROZEN — IT IS SELF-GOVERNING. A sealed roll would have made // a lost key permanent: every proposal needs a strict majority of the whole roll, so a dead // seat is a vote against everything, and a roll of five with three dead keys could never pay // anything again. So the two membership actions the steward loses become proposals the // members vote on, at the same strict majority, with the same frozen electorate. What that // costs is stated plainly: a majority of the roll can admit anybody, including addresses one // member controls. A DAO's members can always vote to do something foolish; the property // this template keeps is that NO SINGLE KEY CAN. What it cannot survive is losing a majority // of its keys at once — at that point nothing passes, including the removals that would fix // it, and the balance is unreachable forever, because the code is immutable and there is no // administrator anywhere in SourceLess who can reach into a contract's slots. Size the roll // for that, and read `members()` as the denominator it is. // // THE IDENTITY RULE — CORRECTED 2026-09-08 // This block described a rule that no longer exists. `ValueJournal.receiveRefusal` used to // require a payment recipient to hold an ALLOCATED STR.domain or be a deployed contract; it // now refuses only Hnitbjorg reserved accounts. The gate became MORE permissive, so nothing // built against the old text is unsafe — but it instructed issuers to pre-flight a check that // no longer applies, and told them a payout would revert when it will not. // // Today: any well-formed, non-reserved address, or a deployed contract, may be paid. What a // recipient with no proved domain still cannot do is SPEND what it receives — a fact about // that holder, not about this contract. // // HOW IT IS FUNDED // By plain STR transfers to its address; a call record carries no value. The contract // cannot see who funded it, so contributing confers no claim and no vote. Membership is the // only thing that confers a vote, which is deliberate: a treasury that gave votes for // money would be computing voting weight over an unauthenticated set. // // HOW IT IS TYPICALLY MISUSED // * Assuming a proposal that has passed will still be executable later. It expires. That is // on purpose: an approved payment sitting unexecuted for a year was approved by a roll // that may no longer exist. // * Admitting an address that is a contract. A contract cannot call anything, so a contract // member can never vote — the seat is dead weight that still counts toward the majority // every proposal needs. The same is true of any key that is lost after renunciation, and // there is no way to retire either. // * Funding before renouncing, then wondering why `propose` reverts with "the steward still // holds the roll". The order is: admit, renounce, fund, propose. Money sent earlier is not // lost — nothing can move it until the roll is final, which is the point — but it sits in // a contract whose electorate one key is still choosing. // * Voting on a membership proposal without reading `kindOf(id)`. An admission and a removal // both carry `amountOf(id) == 0`, so a member who only checks the amount sees a proposal // with nothing at stake and votes it through. The address is in `recipientOf(id)` and the // kind is in `kindOf(id)`; a wallet that shows a DAO proposal must show both. // * Under-declaring the gas limit. MEASURED: admit 1,509, remove 1,386, renounceSteward 673, // propose 2,725, proposeAdmit 2,786, proposeRemove 2,838, vote 1,199, execute 3,338 on a // payment, 2,051 on an admission and 1,994 on a removal. Declare at least 34,000. // ========================================================================================= contract DaoTreasury { // ---- edit before deploying ----------------------------------------------------------- // How long a proposal stays open, in milliseconds. 7 days. state votingPeriodMs: uint = 604800000; // The smallest roll this treasury will govern with. A roll of one is a majority of one, and // a roll of two is a majority of two — neither is a vote, and both are a single key holding // the whole balance while looking like a DAO in the ABI. The floor is enforced where it // matters: `renounceSteward` refuses to seal a roll below it, `remove` refuses to take the // roll below it, `execute` refuses a voted removal that would breach it, and the three // propose functions refuse to put a question to a roll below it. Raise it; do not lower it // below 3. state minRollSize: uint = 3; // ------------------------------------------------------------------------------------- state steward: address = caller; state stewardActive: bool = true; state memberCount: uint = 0; // Bumped on every membership change. A proposal remembers the epoch it was created in, and // eligibility is answered against THAT epoch rather than against the roll as it is now. state epoch: uint = 1; state proposalCount: uint = 0; // The epoch a member was admitted in. 0 means never admitted. state joinedAt: map; // The epoch a member was removed in. 0 means still a member. state leftAt: map; // One proposal is a row across these maps. AresLang has no structs. // 1 = pay, 2 = admit, 3 = remove. Those are the only three things this treasury can decide, // and they are deliberately the same three the Althing has: set the parameters of the thing // you govern, admit a member, remove a member. A proposal kind that could write arbitrary // state is a governance system that can be used to steal. state proposalKind: map; // The address the proposal names: the recipient of a payment, or the member an admission or // a removal is about. state payTo: map; // Zero on an admission and on a removal. state payAmount: map; state rollAt: map; state rollSize: map; state yesVotes: map; state closesAt: map; state executed: map; state voted: map>; event Admitted(who: address, epoch: uint, members: uint); event Removed(who: address, epoch: uint, members: uint); event StewardRenounced(previous: address, members: uint); event Proposed(id: uint, kind: uint, by: address, to: address, amount: uint, roll: uint, closesAt: uint); event Voted(id: uint, by: address, yes: uint, roll: uint); event Executed(id: uint, to: address, amount: uint); // ---- the roll --------------------------------------------------------------------------- fn admit(who: address) -> uint { require(stewardActive == true, "the roll is closed"); require(caller == steward, "not the steward"); // The steward names the roll, so the steward must not be on it. Without this line the // shortest attack on this contract is four calls: remove every member, admit yourself, // propose a payment to yourself, vote once — one yes against a roll of one is a strict // majority — and execute. MEASURED: the whole treasury, and the chain stayed valid. require(who != steward, "the steward cannot sit on the roll"); // And this contract cannot sit on its own roll. A contract can never be `caller`, so // such a member is a seat that can never vote: it counts towards the roll a majority is // measured against and can never be part of one, and after `renounceSteward` the only // way to remove it is a vote by the roll it is handicapping. require(who != self, "self cannot sit on the roll"); require(joinedAt[who] == 0 || leftAt[who] > 0, "already a member"); epoch = epoch + 1; joinedAt[who] = epoch; // Cleared, so a re-admitted member is a member again from this epoch onwards and is // still correctly excluded from proposals created while they were out. leftAt[who] = 0; memberCount = memberCount + 1; emit Admitted(who, epoch, memberCount); return memberCount; } fn remove(who: address) -> uint { require(stewardActive == true, "the roll is closed"); require(caller == steward, "not the steward"); // The floor, on the way down. Shrinking a roll is the other half of packing it: a steward // that can remove members can remove everyone who disagrees until the remainder is a // majority. While the steward is active nothing can be proposed at all (see `propose`), so // this bound is not load-bearing on its own — it is here so that it stays true if that // ever changes. The cost is that a member admitted by mistake cannot be removed until the // roll is ABOVE the floor: admit the rest of the roll first, then remove. require(memberCount > minRollSize, "the roll cannot fall below its floor"); require(joinedAt[who] > 0 && leftAt[who] == 0, "not a member"); epoch = epoch + 1; leftAt[who] = epoch; memberCount = memberCount - 1; emit Removed(who, epoch, memberCount); return memberCount; } // One-way, and the hinge of the whole template. Before it, the roll is one key's to build // and nothing can be proposed; after it, the treasury can pay and the roll belongs to its own // members, who change it with `proposeAdmit` and `proposeRemove` at the same strict majority // a payment needs. The steward keeps nothing: `admit`, `remove` and this function all refuse // a closed roll, and it was never a member, so it has no vote either. fn renounceSteward() -> uint { require(stewardActive == true, "the roll is closed"); require(caller == steward, "not the steward"); // This is the moment the roll becomes final and the treasury becomes spendable, so it is // the moment the floor has to hold. An empty roll would freeze the treasury forever; a // roll of one or two would hand it to one or two keys. require(memberCount >= minRollSize, "the roll is below its floor"); stewardActive = false; emit StewardRenounced(steward, memberCount); return memberCount; } // ---- proposals --------------------------------------------------------------------------- fn propose(to: address, amount: uint) -> uint { // NOTHING CAN BE PROPOSED WHILE THE STEWARD CAN STILL CHANGE THE ROLL, and this one line // is what makes "the steward cannot spend" a fact rather than a hope. A steward that can // admit members can admit members it controls, and no contract can tell an independent // member from an address the steward holds the key to — so any guard short of this one // only raises the number of calls a drain takes. `execute` can only pay a proposal, and // `propose` is the only thing that creates one, so while `stewardActive` is true no value // can leave this contract by any path. `renounceSteward` is one-way, so afterwards the // steward has no power here at all. require(stewardActive == false, "the steward still holds the roll"); // A payment to this contract can win its majority and then revert on every execution, // because the value ledger refuses a contract that pays itself. Refused where it costs // one call, rather than after a whole roll has voted on it. require(to != self, "self is not payable"); require(joinedAt[caller] > 0 && leftAt[caller] == 0, "not a member"); // The floor, tested where the question is put. `renounceSteward` refuses to seal a roll // below it and `execute` refuses a removal that would breach it, so this cannot fail // today — it is the check that keeps the majority test in `execute` meaningful if either // of those is ever edited. require(memberCount >= minRollSize, "the roll is below its floor"); require(amount > 0, "amount must be positive"); // ---- PROPOSAL BEGIN (copy 1 of 3) ---- // The three propose functions differ only in the kind they record and the arguments they // check. Everything below is copied verbatim between the PROPOSAL BEGIN and PROPOSAL END // markers because AresLang has no user-defined functions. IF YOU CHANGE ONE, CHANGE ALL // THREE: a proposal whose roll is frozen on one path and not on another is the whole // vulnerability this template exists to avoid. A test extracts all three from this file // and fails if they are not character-for-character the same apart from the copy number. let id = proposalCount + 1; proposalCount = id; // The freeze. Both halves matter: the epoch decides WHO may vote, and the size decides // HOW MANY yes votes it takes. Neither can move afterwards. rollAt[id] = epoch; rollSize[id] = memberCount; yesVotes[id] = 0; closesAt[id] = timestamp + votingPeriodMs; executed[id] = false; // ---- PROPOSAL END ---- proposalKind[id] = 1; payTo[id] = to; payAmount[id] = amount; emit Proposed(id, 1, caller, to, amount, memberCount, closesAt[id]); return id; } // The roll's own two membership actions, which is how the roll changes at all once the // steward is gone. Both go through the same vote a payment does, so no single key — the // retired steward's included — can put an address on the roll or take one off it. The // members may vote to admit the former steward, or to pay it; that is their decision and not // its power, which is the distinction this whole template turns on. fn proposeAdmit(who: address) -> uint { require(stewardActive == false, "the steward still holds the roll"); // The same seat-that-cannot-vote, by the other door. `admit` refuses it too. require(who != self, "self cannot sit on the roll"); require(joinedAt[caller] > 0 && leftAt[caller] == 0, "not a member"); require(memberCount >= minRollSize, "the roll is below its floor"); require(joinedAt[who] == 0 || leftAt[who] > 0, "already a member"); // ---- PROPOSAL BEGIN (copy 2 of 3) ---- // The three propose functions differ only in the kind they record and the arguments they // check. Everything below is copied verbatim between the PROPOSAL BEGIN and PROPOSAL END // markers because AresLang has no user-defined functions. IF YOU CHANGE ONE, CHANGE ALL // THREE: a proposal whose roll is frozen on one path and not on another is the whole // vulnerability this template exists to avoid. A test extracts all three from this file // and fails if they are not character-for-character the same apart from the copy number. let id = proposalCount + 1; proposalCount = id; // The freeze. Both halves matter: the epoch decides WHO may vote, and the size decides // HOW MANY yes votes it takes. Neither can move afterwards. rollAt[id] = epoch; rollSize[id] = memberCount; yesVotes[id] = 0; closesAt[id] = timestamp + votingPeriodMs; executed[id] = false; // ---- PROPOSAL END ---- proposalKind[id] = 2; payTo[id] = who; payAmount[id] = 0; emit Proposed(id, 2, caller, who, 0, memberCount, closesAt[id]); return id; } fn proposeRemove(who: address) -> uint { require(stewardActive == false, "the steward still holds the roll"); require(joinedAt[caller] > 0 && leftAt[caller] == 0, "not a member"); require(memberCount >= minRollSize, "the roll is below its floor"); require(joinedAt[who] > 0 && leftAt[who] == 0, "not a member"); // ---- PROPOSAL BEGIN (copy 3 of 3) ---- // The three propose functions differ only in the kind they record and the arguments they // check. Everything below is copied verbatim between the PROPOSAL BEGIN and PROPOSAL END // markers because AresLang has no user-defined functions. IF YOU CHANGE ONE, CHANGE ALL // THREE: a proposal whose roll is frozen on one path and not on another is the whole // vulnerability this template exists to avoid. A test extracts all three from this file // and fails if they are not character-for-character the same apart from the copy number. let id = proposalCount + 1; proposalCount = id; // The freeze. Both halves matter: the epoch decides WHO may vote, and the size decides // HOW MANY yes votes it takes. Neither can move afterwards. rollAt[id] = epoch; rollSize[id] = memberCount; yesVotes[id] = 0; closesAt[id] = timestamp + votingPeriodMs; executed[id] = false; // ---- PROPOSAL END ---- proposalKind[id] = 3; payTo[id] = who; payAmount[id] = 0; emit Proposed(id, 3, caller, who, 0, memberCount, closesAt[id]); return id; } // A yes vote. There is no no-vote to record: a proposal needs a strict majority of the // whole frozen roll, so not voting IS voting against. fn vote(id: uint) -> uint { require(id > 0 && id <= proposalCount, "no such proposal"); require(executed[id] == false, "already executed"); require(timestamp < closesAt[id], "voting has closed"); // Eligibility against the FROZEN epoch, never against the roll as it is now. A member // admitted after this proposal was created has joinedAt > rollAt and cannot vote on it; // a member removed after it was created has leftAt > rollAt and still can. require(joinedAt[caller] > 0, "not a member"); require(joinedAt[caller] <= rollAt[id], "admitted after this proposal"); require(leftAt[caller] == 0 || leftAt[caller] > rollAt[id], "removed before this proposal"); require(voted[id][caller] == false, "already voted"); voted[id][caller] = true; let count = yesVotes[id] + 1; yesVotes[id] = count; emit Voted(id, caller, count, rollSize[id]); return count; } // ANYONE may trigger execution, deliberately: it can only ever do what the frozen roll // already approved, to the address the proposal already named, once. Gating it on // membership would add no authority and would let a payment stall because the members who // voted for it had all gone quiet. (The multisig template makes the opposite choice, and // says so; there the owner set is the whole of the contract's identity.) // // Returns the amount paid for a payment, and the resulting member count for an admission or // a removal. fn execute(id: uint) -> uint { require(id > 0 && id <= proposalCount, "no such proposal"); require(executed[id] == false, "already executed"); require(timestamp < closesAt[id], "proposal expired"); // A strict majority of the roll as it was when the proposal was made. Not of the roll as // it is now, and not of the members who happened to turn up. require(yesVotes[id] * 2 > rollSize[id], "no majority"); // Set before any effect and before the transfer. A revert unwinds it with everything else, // so a proposal the ledger refused to pay is still executable; a proposal that paid is // never executable again. executed[id] = true; let who = payTo[id]; if (proposalKind[id] == 1) { let amount = payAmount[id]; require(balance(self) >= amount, "insufficient balance"); transfer(who, amount); emit Executed(id, who, amount); return amount; } if (proposalKind[id] == 2) { // Re-checked HERE, against the roll as it is now, and not only at proposal time: two // admissions of the same address could both pass, and the second must not double-count // the member. The same for a removal of somebody already gone. require(joinedAt[who] == 0 || leftAt[who] > 0, "already a member"); epoch = epoch + 1; joinedAt[who] = epoch; leftAt[who] = 0; memberCount = memberCount + 1; emit Admitted(who, epoch, memberCount); return memberCount; } // Kind 3, by elimination and not by accident: `id <= proposalCount` means the row was // written by one of the three propose functions, and each of them writes its kind before // it returns, so there is no fourth value this can be. require(joinedAt[who] > 0 && leftAt[who] == 0, "not a member"); // The floor holds on this path too. A roll that can vote itself down to one has voted // itself into a single key holding the balance. require(memberCount > minRollSize, "the roll cannot fall below its floor"); epoch = epoch + 1; leftAt[who] = epoch; memberCount = memberCount - 1; emit Removed(who, epoch, memberCount); return memberCount; } // ---- reads ------------------------------------------------------------------------------ view fn stewardIs() -> address { return steward; } view fn stewardHasPower() -> bool { return stewardActive; } view fn members() -> uint { return memberCount; } // The floor this treasury refuses to govern below. Readable before anyone funds it. view fn minimumRoll() -> uint { return minRollSize; } view fn currentEpoch() -> uint { return epoch; } view fn isMember(who: address) -> bool { return joinedAt[who] > 0 && leftAt[who] == 0; } view fn held() -> uint { return balance(self); } view fn proposals() -> uint { return proposalCount; } // 1 = pay, 2 = admit, 3 = remove. Read it before you vote: the amount is zero on the two // membership kinds, so an eye that only checks `amountOf` sees nothing at stake. view fn kindOf(id: uint) -> uint { return proposalKind[id]; } // The recipient of a payment, or the member an admission or a removal is about. view fn recipientOf(id: uint) -> address { return payTo[id]; } view fn amountOf(id: uint) -> uint { return payAmount[id]; } view fn rollOf(id: uint) -> uint { return rollSize[id]; } view fn epochOf(id: uint) -> uint { return rollAt[id]; } view fn yesOf(id: uint) -> uint { return yesVotes[id]; } view fn closingOf(id: uint) -> uint { return closesAt[id]; } view fn wasExecuted(id: uint) -> bool { return executed[id]; } view fn hasVoted(id: uint, who: address) -> bool { return voted[id][who]; } }