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
| Entrypoint | Signature | Flags |
|---|---|---|
init | (token_sale: option(TokenSale), factory_address: address) => DAO.state | stateful |
add_vote | (metadata: DAOVote.metadata) => tuple(int, DAOVote) | stateful, payable |
apply_vote_subject | (vote_seq_id: int) => unit | stateful |
record_sale_transaction | (buyer: address, buy_price: int) => unit | stateful, payable |
set_token_sale | (token_sale: TokenSale) => unit | stateful |
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 idCreating 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 moderatorIn 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_percentageisyes / (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 = 55and pass; 109 give54and fail. - Both comparisons are
>=. Exactly 55% and exactly 10% pass.
The full precondition set for execution, in the order the contract checks them:
| Gate | Abort string | Meaning |
|---|---|---|
| Not already applied | VOTE_ALREADY_APPLIED | Someone already executed this vote |
| Within the execution window | VOTE_RESULT_TIMEOUTED | More than vote_timeout key blocks have passed since close height |
| Vote has closed | VOTE_NOT_YET_FINAL | final_vote_state() returns None while the vote is still open |
| Somebody voted | NO_VOTE_STAKE | Zero total stake; also guards the division below |
| Token supply is non-zero | NO_TOKEN_SUPPLY | Guards the quorum division |
| At least 55% in favour | GREATER_55_PERCENT_AGREEMENT_REQUIRED | yes * 100 / (yes + no) >= 55 |
| At least 10% participation | GREATER_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 daysAt æ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
- DAOVote: the proposal contract
add_votedeploys - CommunityManagement: the source of the token threshold
add_votechecks - Proposals: the subjects explained for a non-developer audience
- Quorum and thresholds: the passing conditions
- Replaceable DAO: the migration path
ChangeDAOimplements