SourceLessExplorer · SourceLess AsgardSourceLess Asgard · chain 1313
LiveHeight23,718Head age6 sLast interval1 minFinality23,707Behind−11Pending0 / 8,192Tx in head0
XPlorer following the chain as its own read-only node

height 23718 of 23718 on the network. 9 peer(s) connected, 77 in its peer directory; 1014 block(s) followed and 843 pulled in by sync since this follower started. Caught up: this follower holds every block any peer has claimed, so every panel below is the whole chain. Genesis 1cf8655125cc1bbb…, producers genesis.

What this follower is, locally

Following 7 seed(s) — their addresses are not published here, for the reason the peer table gives. Its store is held under its own Cerberus lease — no other node’s store is opened, touched or stopped.

DaoTreasury for your fork

Status
a draft, not deployed nothing here is on any chain; never send value to a draft
Profile
native STR treasury
Holds
native STR
The property that matters
the electorate is frozen at proposal time, in both directions

Edit the draft

Set the constants the template marks for editing, and name a deployer to see where it would land.

uint, shipped as 604800000

uint, shipped as 3

The form asks this explorer to compile your draft and show it back. It signs nothing, stores nothing and reaches no node.

The draft as shipped

Contract
DaoTreasury
Compiler
areslang-1.3.0
Bytecode SHA-256
d67bfdc5d9812d8cd4a7843279a014d6519fd5c94adf608d4434d3692d86b24a
Bytecode
5,807 bytes
Source SHA-256
ccef55fc383098fd9ebb7815a2d8792f247f8e2c06ee176413a3862d95393e24
The catalogue text?
Yes, byte for byte: matches the published vector
Ready to deploy?
no catalogue examples left
Would land at
name a deployer to see it

Deploy it to your fork

Only the last step signs, and only it reaches a node.

  1. Save the draft. Download it above, or start from the catalogue text with strblk contract template dao-treasury > dao-treasury.ares and edit the marked constants yourself.
  2. Dry-run it on your machine. strblk wallet deploy dao-treasury.ares --dry-run --address <your address on the fork> compiles it locally and prints its digest and address. They must match this page; if they do not, the file is not this draft.
  3. Find your fork's RPC. It is the rpc (Iris, wallets) line of the fork's banner, and the rpcUrl of each node in <data root>/fork.json.
  4. Deploy it. strblk wallet deploy dao-treasury.ares --node <your fork's RPC>, from a wallet whose account holds a proved STR.domain on your fork. Before it signs, the wallet prints the network: the realm reads unregistered and the genesis is your fork's hash. Check that hash; it is what the signature binds.

Measured gas

Charged by the STARW VM under areslang-1.3.0, the same on your fork. Measured, not estimated.

Entry pointGas
execute3,338
proposeRemove2,838
proposeAdmit2,786
propose2,725
execute(admit)2,051
execute(remove)1,994
admit1,509
remove1,386
vote1,199
renounceSteward673
Declare at least
33,380 ten times the most expensive call

Functions

KindNameInputsOutputsMutability
functionadmitwho: addressuintnonpayable
functionremovewho: addressuintnonpayable
functionrenounceStewarduintnonpayable
functionproposeto: address, amount: uintuintnonpayable
functionproposeAdmitwho: addressuintnonpayable
functionproposeRemovewho: addressuintnonpayable
functionvoteid: uintuintnonpayable
functionexecuteid: uintuintnonpayable
functionstewardIsaddressview
functionstewardHasPowerboolview
functionmembersuintview
functionminimumRolluintview
functioncurrentEpochuintview
functionisMemberwho: addressboolview
functionhelduintview
functionproposalsuintview
functionkindOfid: uintuintview
functionrecipientOfid: uintaddressview
functionamountOfid: uintuintview
functionrollOfid: uintuintview
functionepochOfid: uintuintview
functionyesOfid: uintuintview
functionclosingOfid: uintuintview
functionwasExecutedid: uintboolview
functionhasVotedid: uint, who: addressboolview
eventAdmittedwho: address, epoch: uint, members: uint
eventRemovedwho: address, epoch: uint, members: uint
eventStewardRenouncedprevious: address, members: uint
eventProposedid: uint, kind: uint, by: address, to: address, amount: uint, roll: uint, closesAt: uint
eventVotedid: uint, by: address, yes: uint, roll: uint
eventExecutedid: uint, to: address, amount: uint

Your draft's source

templates/dao-treasury.ares, as shipped

// =========================================================================================
// 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<address, uint>;
  // The epoch a member was removed in. 0 means still a member.
  state leftAt: map<address, uint>;

  // 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<uint, uint>;
  // The address the proposal names: the recipient of a payment, or the member an admission or
  // a removal is about.
  state payTo: map<uint, address>;
  // Zero on an admission and on a removal.
  state payAmount: map<uint, uint>;
  state rollAt: map<uint, uint>;
  state rollSize: map<uint, uint>;
  state yesVotes: map<uint, uint>;
  state closesAt: map<uint, uint>;
  state executed: map<uint, bool>;
  state voted: map<uint, map<address, bool>>;

  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];
  }
}