Governance & Voting
Polls

Polls

A poll is a contract. Each one is deployed on its own, registered in the Registry, and holds its own ballot. It does not hold the result: the contract records one option per address, and the weight behind those addresses is computed off-chain from AE balances and delegations at the poll's close height.

The official result

stake_weighted_results is the official outcome. raw_vote_counts are address counts only.

The Poll contract stores votes : map(address, int): one option per address, with no weight attached. Counting those entries tells you how many addresses picked each option, which can point the opposite way from the stake-weighted result: a hundred addresses holding dust outvote two whales on raw counts and lose decisively on stake.

When stake_weighted_results is null, the governance backend has no result for that poll. Do not fall back on raw counts.

The data model

On the poll contract

Poll.state = { metadata      : Poll.metadata
             , vote_options  : map(int, string)
             , close_height  : option(int)
             , create_height : int
             , votes         : map(address, int)
             , author        : address }

Poll.metadata = { title       : string
                , description : string
                , link        : string
                , spec_ref    : option(hash) }
FieldNotes
vote_optionsOption index to label. Indices are 0-based and fixed at deployment.
close_heightA key-block height, or None for a poll that never closes.
votesOne entry per address: the option index they chose. No weight.
authorWhoever deployed the poll. Carries no special power over it.
spec_refAn optional hash reference to an off-chain specification. Superhero's creation paths leave it unset.

Nothing is editable after deployment. There is no change_metadata, no way to add an option, and no way to extend or shorten close_height. The only remedy for a mistake is a new poll.

The contract validates the metadata lengths in init: the title must be at most 50 characters (TITLE_STRING_TO_LONG) and the description at most 300 (DESCRIPTION_STRING_TO_LONG). The link, spec_ref, the option labels, the number of options and the close height are not validated on-chain. A poll deployed with a close height already in the past is born closed.

On the registry

The Registry stores a thin summary per poll so that listings do not require calling every poll contract:

Registry.poll_data = { poll         : Poll
                     , is_listed    : bool
                     , title        : string
                     , close_height : option(int) }

polls() returns them all keyed by sequential id; poll(poll_id) returns one. See The Governance Registry.

Listed and unlisted polls

is_listed is set when the poll is registered, by the second argument to add_poll(poll, is_listed). It is a listing flag, not a privacy flag:

  • An unlisted poll is hidden from the default listing in the app and in the CLI.
  • It is fully readable on-chain by anyone who knows its address or its registry id.
  • Anyone can vote on it.
  • It still appears in Registry.polls(), which returns everything.

Use it for polls that should not clutter the public list (a test, a narrow question for a specific group), not for anything that needs to stay secret.

The CLI defaults to listed-only and takes --all to include unlisted polls:

node scripts/superhero-governance.mjs list          # listed only, 20 most recent
node scripts/superhero-governance.mjs list 50       # listed only, 50 most recent
node scripts/superhero-governance.mjs list 10 --all # include unlisted

Read a poll

node scripts/superhero-governance.mjs info <poll_address>
node scripts/superhero-governance.mjs info-by-id <poll_id>

info takes the poll's ct_ contract address; info-by-id takes the registry's sequential id and resolves it first. Both print the same record: the metadata, the option labels and their indices, the raw counts, the official stake-weighted results when available, whether the poll is closed, and your own current vote.

{
  "poll_id": "42",
  "poll_address": "ct_...",
  "is_listed": true,
  "title": "Should we upgrade to protocol v3?",
  "author": "ak_...",
  "create_height": "1234567",
  "close_height": "1500000",
  "is_closed": false,
  "vote_options": { "0": "Yes", "1": "No", "2": "Abstain" },
  "raw_vote_counts": { "0": 42, "1": 15, "2": 3 },
  "total_raw_votes": 60,
  "stake_weighted_results": {
    "total_stake": "123000000000000000000",
    "percent_of_total_supply": "0.1",
    "raw_vote_count": 60,
    "options": [
      {
        "option": "0",
        "option_stake": "100000000000000000000",
        "percentage_of_total_stake": "81.3",
        "raw_vote_count": 42,
        "delegators_count": 7
      }
    ]
  },
  "my_vote": { "has_voted": true, "option": "0" }
}

total_stake and option_stake are in aettos; divide by 10^18 for AE. percent_of_total_supply is how much of all AE in existence took part, which is the closest thing this system has to a turnout figure. There is no quorum: a poll with 0.1% participation still produces a result.

Poll entrypoints

EntrypointPurpose
get_state()The whole Poll.state record
title(), metadata()Poll identity
close_height()The closing height, as option(int)
votes()Every address and the option it chose
vote(option)Cast or change your vote (stateful)
revoke_vote()Retract your vote (stateful)
has_voted(voter)Whether that address has a vote standing
voted_option(voter)Which option, as option(int)
is_closed()Whether the closing height has passed
is_author(author)Whether that address deployed the poll
version()Contract version

The contract emits Vote (the poll address, the voter and the option index) and RevokeVote (the poll address and the voter). The events are on each poll, not on the registry, so an indexer subscribes per poll.

How stake weighting works

A poll is counted with the state at its closing height, considering the active delegations and account stakes at that height.

  • Your weight is your AE balance, not a token balance and not a staked position.
  • It is measured at the close height, not when you voted. Buying AE after voting increases your weight; spending it reduces it.
  • Accounts that have not voted on the poll have their AE counted for whoever they delegated to. Voting yourself overrides your delegation for that poll only.
  • Delegation chains resolve transitively: a delegatee who has themselves delegated passes the weight along. See Delegation.

An open-ended poll (close_height set to None) has no snapshot. It never closes, so its result is a running figure computed against the current state. Treat it as continuous sentiment, not a decision.

None of this is computed on-chain. The Poll contract cannot see AE balances or the Registry's delegation map, which is why the official figure comes from the backend.

The governance API

The governance backend indexes the contracts and computes the stake-weighted result.

NetworkBase URL
Mainnethttps://governance-server-mainnet.prd.service.aepps.com/
Testnethttps://governance-server-testnet.prd.service.aepps.com/
EndpointReturns
GET pollOverview/:pollAddressPoll state, per-account stakes at height, per-option stakes, total stake, vote count
GET votesState/:addressThe same overview shape. This is the source of stake_weighted_results
GET pollOrdering?closed=true,falseThe backend's ordering of polls, with per-poll totals and a score
GET delegatedPower/:address?poll=:pollAddressDelegated power for an account, with its delegation tree
GET accountPollVoterAuthor/:addressPolls an account voted in, authored, or was voted in by its delegatee
GET versionBackend version
POST contractEventNudge the backend to re-index after a vote or delegation, with a topic and an optional poll

The app calls contractEvent immediately after every vote, revoke and delegation so the backend re-indexes promptly. If your own client skips it, expect a lag before your vote shows up in the stake-weighted figures.

pollOverview carries a stakesAtHeight array with one entry per voting account: account, option, stake, balance, delegated, delegators and a delegationTree. Look there when a result does not match your expectation: it separates an account's own balance from the weight it received by delegation.

The agent skill's CLI defaults to https://governance-server-mainnet.prd.aepps.com for mainnet, overridable with AE_GOVERNANCE_BACKEND_URL. If one host does not answer, try the other.

Through the Superhero backend

Superhero's own API indexes governance transactions separately, which is the better source for history:

EndpointReturns
GET /api/governance/votesPaginated polls, with votes_count, votes_revoked_count and votes_count_by_option
GET /api/governance/votes/:pollAddressOne poll with all of its votes
GET /api/governance/delegationsPaginated delegations; includeRevoked to see revoked ones
GET /api/governance/delegations/:accountAddressDelegation and revocation history for one account, in order

These are indexed transaction records: counts of votes and revocations, not stake weights. Use them for provenance and history, and the governance API for results.

Related