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
| Entrypoint | Signature | Flags |
|---|---|---|
init | (metadata: DAOVote.metadata, token: FungibleTokenFull, author: address) => DAOVote.state | |
vote | (option: DAOVote.vote_option, amount: DAOVote.amount) => unit | stateful |
revoke_vote | () => unit | stateful |
withdraw | () => unit | stateful |
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 * 260 / 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_heightNote 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() | |
|---|---|---|
| When | While the vote is open | After the close height |
| Height gate | require(!is_closed(), "VOTE_ALREADY_CLOSED") | require(is_closed(), "VOTE_NOT_CLOSED") |
| Effect on the tally | Removes your stake from the option you chose | None; the result is already final |
| Repeatable | Yes, after voting again | No (AMOUNT_ALREADY_WITHDRAWN) |
| Use it to | Change your mind, or exit before close | Reclaim 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 type | vote_state | option(vote_state) |
| Before close height | The running tally, changing with every vote and revoke | None |
| After close height | Still readable | Some(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 theapply_vote_subjectcall 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
- DAO: the contract that deploys these, runs the thresholds, and executes the result
- FungibleTokenFull (AEX-9): the allowance calls voting depends on
- Voting: the same flow from the app
- Quorum and thresholds: what it takes for a proposal to pass
- Calling Contracts: SDK setup and revert handling