Smart Contracts
DAOVote

DAOVote

DAOVote.aes is one proposal. A fresh instance is deployed by DAO.add_vote for every vote, so its address is the proposal's identity. It holds the subject being voted on, the tally, and the community tokens voters have locked behind each side.

Its interface is inlined in CommunityFactory.aci.json.

DAOVote counts stake and gates on height. The quorum and supermajority checks are in DAO.apply_vote_subject. See Passing conditions.

Entrypoints

EntrypointSignatureFlags
init(metadata: DAOVote.metadata, token: FungibleTokenFull, author: address) => DAOVote.state
vote(option: DAOVote.vote_option, amount: DAOVote.amount) => unitstateful
revoke_vote() => unitstateful
withdraw() => unitstateful
current_vote_state() => DAOVote.vote_state
final_vote_state() => option(DAOVote.vote_state)
has_voted(voter: address) => bool
voted_option(voter: address) => option(DAOVote.vote_option)
has_withdrawn(voter: address) => option(bool)
is_closed() => bool
close_height() => int
subject() => VoteSubjects.subject
metadata() => DAOVote.metadata
token() => FungibleTokenFull
vote_accounts() => DAOVote.vote_accounts
version() => int
get_state() => DAOVote.state

Types

type vote_option = bool                  // true = in favour, false = against
type amount      = int                   // token base units, 18 decimals

type vote_state    = map(vote_option, amount)
type vote_accounts = map(address, (amount * vote_option * bool))
                  //                         ^ voted    ^ withdrawn?

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

record state =
  { metadata      : metadata
  , close_height  : int
  , create_height : int
  , vote_state    : vote_state
  , vote_accounts : vote_accounts
  , token         : FungibleTokenFull
  , author        : address }

vote_option is bool. Every proposal is a yes/no on its subject. Pass true to support and false to oppose. (æternity governance polls differ: Poll.vote_option is map(int, string) and a poll can carry any number of named options. See Registry and Poll.)

amount is int, in token base units. Community tokens have 18 decimals, so voting with one whole token means passing 10 ** 18. Your voting weight is the number of tokens you lock.

vote_accounts maps each voter to a three-element tuple, decoded in JavaScript as an array:

const accounts = await vote.vote_accounts().then((r) => r.decodedResult);
const [amount, option, withdrawn] = accounts.get('ak_alice...');

Events

datatype event =
    Vote(address, vote_option, amount)
  | RevokeVote(address, vote_option, amount)
  | Withdraw(address, amount)

Close height

The close height is set at deployment:

close_height = Chain.block_height + (60 / 3) * 24 * 7 * 2

60 / 3 is one hour in key blocks at a three-minute key block time, so the expression is 20 × 24 × 7 × 2 = 6,720 key blocks, about two weeks. Voting is open until the chain reaches that height and closed from then on. The window is measured in key blocks, not wall-clock time: if block production slows, the vote stays open longer in real time.

is_closed() is the single predicate every height gate goes through:

entrypoint is_closed() : bool = Chain.block_height >= state.close_height

Note the >=: the vote is closed at close_height, not after it.

init caps the proposal text with require(String.length(metadata.description) =< 300, "DESCRIPTION_STRING_TOO_LONG"). A description over 300 characters makes DAO.add_vote abort, taking the whole 555 AE transaction with it. link is unchecked.

The allowance you need before voting

vote locks your tokens in the vote contract by pulling them with the AEX-9 allowance mechanism. Grant that allowance on the token contract first, to the vote contract's address rewritten from ct_ to ak_:

const forAccount = voteAddress.replaceAll('ct_', 'ak_');
const amount = new BigNumber(10).shiftedBy(18).toFixed(0);   // 10 whole tokens
 
const existing = await token
  .allowance({ from_account: aeSdk.address, for_account: forAccount })
  .then((r) => r.decodedResult)
  .catch(() => undefined);
 
if (existing === undefined) {
  await token.create_allowance(forAccount, amount);
} else {
  const delta = new BigNumber(amount).minus(existing.toString()).toFixed();
  await token.change_allowance(forAccount, delta);
}
 
await vote.vote(true, amount, { omitUnknown: true });

allowance returns option(int), so a never-granted allowance decodes to undefined and needs create_allowance; an existing one is adjusted with change_allowance, which takes a delta. A missing allowance is the most common reason a vote call reverts.

Locking is what makes the tally meaningful: the tokens cannot be transferred, sold back to the curve, or used in another concurrent vote while they are held here.

Retracting and reclaiming

There are two ways to get your tokens back, and they differ in when they work and what they do to the tally.

revoke_vote()withdraw()
WhenWhile the vote is openAfter the close height
Height gaterequire(!is_closed(), "VOTE_ALREADY_CLOSED")require(is_closed(), "VOTE_NOT_CLOSED")
Effect on the tallyRemoves your stake from the option you choseNone; the result is already final
RepeatableYes, after voting againNo (AMOUNT_ALREADY_WITHDRAWN)
Use it toChange your mind, or exit before closeReclaim locked tokens from a finished vote

Both gates are enforced on chain by block height. They are exact complements of each other through is_closed(), so at every height exactly one of the two is callable.

Each also requires that you voted: require(Map.member(Call.caller, state.vote_accounts), "VOTER_NOT_VOTED").

Changing your mind

vote refuses a second vote from the same address:

require(!Map.member(Call.caller, state.vote_accounts), "VOTER_ALREADY_VOTED")

revoke_vote deletes your entry from vote_accounts outright, so the sequence vote, revoke, vote again works, and the second vote may use a different option and a different amount. That is the supported way to switch sides or resize a position while a vote is open. To add to an existing position, revoke first.

has_withdrawn(voter) returns option(bool): None if that address never voted or has revoked, Some(false) if they voted and have not withdrawn, Some(true) if they have.

Tokens stay locked in the vote contract until you call withdraw; nothing returns them automatically.

current_vote_state versus final_vote_state

current_vote_state()final_vote_state()
Return typevote_stateoption(vote_state)
Before close heightThe running tally, changing with every vote and revokeNone
After close heightStill readableSome(tally), the frozen result

DAO.apply_vote_subject switches on final_vote_state() and aborts with VOTE_NOT_YET_FINAL when it gets None, which is what stops a proposal being executed mid-vote on a favourable intermediate tally.

For a progress bar, read current_vote_state(). For anything that decides an outcome, read final_vote_state() and handle the None case.

const current = await vote.current_vote_state().then((r) => r.decodedResult);
const inFavour = current.get(true) ?? 0n;
const against  = current.get(false) ?? 0n;

The map is keyed by the boolean option, and an option nobody has voted for is absent. That is why the code uses ?? 0n.

Passing conditions

A vote must clear both:

  • at least 10% of the token's total supply participating, and
  • at least 55% of the participating stake in favour.

Both tests are in DAO.apply_vote_subject, which reads the frozen tally out of final_vote_state() and does the arithmetic itself:

let total_vote_stake = vote_state[true] + vote_state[false]
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) / get_token().total_supply()
require(stake_percentage >= 10, "GREATER_10_PERCENT_STAKE_REQUIRED")

Three details for anyone predicting an outcome from this contract's state alone:

  • The 55% denominator is votes cast, yes + no, not supply. Non-voters do not count against a proposal.
  • The 10% denominator is total_supply() read at execution time, not at creation or at close. Buys between the close height and the apply_vote_subject call raise the denominator; sells lower it. The short 960-key-block application window limits the effect.
  • Both are truncating integer divisions compared with >=. 55 and 10 exactly pass; one base unit short rounds down and fails.

final_vote_state() tells you the tally. To know whether a proposal will execute you also need get_token().total_supply() at the height you expect execution to happen. See DAO for the full gate list.

Related