Smart Contracts
DAO

DAO

DAO.aes is deployed by the factory as part of every community. It is both the owner and the beneficiary of that community's token sale, so it receives the DAO half of the bonding curve spread and holds administrative authority over the sale. It is also the contract that creates votes and executes them.

Resolve a community's DAO from its sale contract's state (sale.get_state().dao_contract) or from the CreateCommunity event. Its interface is inlined in CommunityFactory.aci.json.

Entrypoints

EntrypointSignatureFlags
init(token_sale: option(TokenSale), factory_address: address) => DAO.statestateful
add_vote(metadata: DAOVote.metadata) => tuple(int, DAOVote)stateful, payable
apply_vote_subject(vote_seq_id: int) => unitstateful
record_sale_transaction(buyer: address, buy_price: int) => unitstateful, payable
set_token_sale(token_sale: TokenSale) => unitstateful
votes() => DAO.votes
vote_timeout() => int
get_token() => FungibleTokenFull
get_token_sale() => TokenSale
get_community_management() => option(CommunityManagement)
factory() => Factory
is_dao() => bool
version() => int
get_state() => DAO.state

Types

type votes = map(int, (bool * DAOVote))
          //          ^ applied?  ^ the vote contract

record state =
  { token_sale   : option(TokenSale)
  , votes        : votes
  , vote_timeout : int                // execution window after close, in key blocks
  , factory      : Factory }

The bool in each votes entry is the "already applied" flag. apply_vote_subject checks it and refuses to run twice on the same entry.

Events

datatype event =
    VoteAdded(DAOVote, int)            // vote contract, seq id
  | AppliedVoteSubject(DAOVote, int)   // vote contract, seq id

Creating a vote

add_vote deploys a fresh DAOVote contract and registers it. It is payable and the amount is checked exactly:

require(Call.value == 555 * (10 ^ 18), "ADD_VOTE_DAO_DONATION_MISSMATCH")

555 AE, no more and no less. The AE goes to the DAO treasury. Unlike collection creation, where the 555 AE is burned, here the community keeps it. That is the anti-spam cost of opening a proposal.

The contract then reads the minimum token threshold from the linked community management contract, aborting with NO_COMMUNITY_MANAGEMENT_AVAILABLE if none is set, and requires the caller to hold at least that many tokens:

require(Option.default(0, token.balance(Call.caller)) >= minimum_token_threshold,
        "ADD_VOTE_TOKEN_HOLDER_MISSMATCH")

The threshold is a balance check at call time. Proposing locks no tokens; only voters lock stake, when they call DAOVote.vote.

add_vote does not check a payout amount against the treasury balance. A VotePayout or VotePayoutAmount for more AE than the DAO holds is accepted and can pass, and then fails at execution when apply_vote_subject reaches Chain.spend. Check the treasury balance before proposing an amount.

const result = await dao.add_vote(
  {
    subject: { VotePayout: ['ak_recipient...'] },
    description: 'Pay the treasury to the community wallet',
    link: 'https://superhero.com/trends/daos',
  },
  { amount: (555n * 10n ** 18n).toString(), omitUnknown: true },
);
 
const [voteSeqId, voteAddress] = result.decodedResult;

DAOVote.metadata

record metadata =
  { subject     : VoteSubjects.subject
  , description : string
  , link        : string }

description is capped at 300 characters; link is free text. subject is the variant that determines what executing the vote does:

datatype subject =
    VotePayout(address)                    // pay the DAO's entire balance to this account
  | VotePayoutAmount(address, int)         // pay this amount (aettos) to this account
  | ChangeDAO(address)                     // hand ownership, beneficiary and funds to another DAO
  | ChangeMetaInfo(map(string, string))    // replace the community's meta_info
  | ChangeMinimumTokenThreshold(int)       // change the write-access threshold
  | AddModerator(address)                  // appoint a moderator
  | DeleteModerator(address)               // remove a moderator

In JavaScript each is a one-key object whose value is an array of the constructor's arguments: { VotePayoutAmount: ['ak_...', '1000000000000000000'] }, and for the map-valued one, { ChangeMetaInfo: [new Map([['description', 'new text']])] }.

The Superhero app's create-vote form offers VotePayout, ChangeDAO, AddModerator and DeleteModerator. All seven subjects are callable directly.

Executing a vote

apply_vote_subject(vote_seq_id) turns a passed vote into a state change. Any account can call it; the DAO does the checking:

let (applied, vote) = state.votes[vote_seq_id]
require(!applied, "VOTE_ALREADY_APPLIED")
require(vote.close_height() + state.vote_timeout >= Chain.block_height, "VOTE_RESULT_TIMEOUTED")
switch(vote.final_vote_state())
  None => abort("VOTE_NOT_YET_FINAL")
  ...

Quorum and supermajority

DAOVote.final_vote_state() does no threshold arithmetic. It returns Some(vote_state) once Chain.block_height >= close_height and None before that. Every percentage check is in DAO.apply_vote_subject:

let total_vote_stake = vote_state[true] + vote_state[false]
require(total_vote_stake > 0, "NO_VOTE_STAKE")

let token_total_supply = get_token().total_supply()
require(token_total_supply > 0, "NO_TOKEN_SUPPLY")

let in_favor_percentage = (vote_state[true] * 100) / total_vote_stake
require(in_favor_percentage >= 55, "GREATER_55_PERCENT_AGREEMENT_REQUIRED")

let stake_percentage = (total_vote_stake * 100) / token_total_supply
require(stake_percentage >= 10, "GREATER_10_PERCENT_STAKE_REQUIRED")
  • The supermajority is measured against votes cast, not supply. in_favor_percentage is yes / (yes + no). Abstaining does not count against a proposal.
  • Quorum is measured against total_supply() read at execution time, not at vote creation and not at close height. Buys before execution raise the denominator; sells lower it. The short application window limits the effect.
  • Both are integer divisions, truncated before comparison. With 200 tokens of stake, 110 yes votes give (110 * 100) / 200 = 55 and pass; 109 give 54 and fail.
  • Both comparisons are >=. Exactly 55% and exactly 10% pass.

The full precondition set for execution, in the order the contract checks them:

GateAbort stringMeaning
Not already appliedVOTE_ALREADY_APPLIEDSomeone already executed this vote
Within the execution windowVOTE_RESULT_TIMEOUTEDMore than vote_timeout key blocks have passed since close height
Vote has closedVOTE_NOT_YET_FINALfinal_vote_state() returns None while the vote is still open
Somebody votedNO_VOTE_STAKEZero total stake; also guards the division below
Token supply is non-zeroNO_TOKEN_SUPPLYGuards the quorum division
At least 55% in favourGREATER_55_PERCENT_AGREEMENT_REQUIREDyes * 100 / (yes + no) >= 55
At least 10% participationGREATER_10_PERCENT_STAKE_REQUIRED(yes + no) * 100 / total_supply >= 10

The execution window

A vote that passed but was not applied within vote_timeout key blocks after its close height can no longer be applied: apply_vote_subject aborts with VOTE_RESULT_TIMEOUTED from then on. To retry, open the proposal again, at the cost of another 555 AE.

The window is 960 key blocks, set once in init:

vote_timeout = (60 / 3) * 24 * 2   // 2 days: (60 / 3) key blocks per hour, 24 hours, 2 days

At æternity's roughly three-minute key block time that is about two days. dao.vote_timeout() returns the raw key-block count. No vote subject changes it, so every DAO the factory deploys carries the same 960.

Execution is open to any account and nobody is obliged to do it. If you care about a proposal passing, call apply_vote_subject yourself once the vote closes.

Receiving sale proceeds

record_sale_transaction(buyer, buy_price) is payable and is how the token sale pays the DAO its half of the spread on every buy. The buyer address and the price come along as arguments so a replacement DAO can implement its own split or accounting. That is the point of the ReplaceableDAO interface, where proceeds arrive through a contract call rather than a bare Chain.spend.

The stock DAO.aes accepts the AE and records nothing further; no event fires. For per-buy attribution, read the sale contract's Buy event. The sale calls this entrypoint with protected = true, so a DAO that aborts here cannot block a buy.

Users never need to call this.

The ChangeDAO safeguard

is_dao() returns true. It exists so that a ChangeDAO vote can check its target before transferring ownership, beneficiary rights and the treasury balance. A target that does not implement is_dao() = true cannot receive a DAO migration, which prevents the treasury being voted into a plain account or an unrelated contract by mistake.

ChangeDAO carries a second guard. After moving the balance and reassigning the sale's beneficiary and owner, it requires the new DAO to point at the same community management contract:

require(get_community_management() == new_dao.get_community_management(),
        "NEW_DAO_HAS_TO_REFERENCE_SAME_COMMUNITY_MANAGEMENT")

Only then does it hand community management's ownership over. A replacement DAO that resolves a different community management contract aborts the whole migration. This is an initial check only. Once installed, a custom DAO can change it.

set_token_sale is creator-only and one-shot

require(Call.caller == Contract.creator, "ONLY_CREATOR_CAN_SET_SALE")
require(Option.is_none(state.token_sale), "TOKEN_SALE_ALREADY_SET")

The factory calls it in the same transaction as the DAO's deployment, to break the sale-needs-DAO / DAO-needs-sale cycle. It can never be called again.

Related