On-chain Poll & Voting Smart Contract for GlobalForce

pollgf

{% note alert %}

This smart contract is presented here as an example, do not use it for production without prior verification.

{% endnote %}

If you don't have a local GlobalForce node installed, use the deployment tools at docs.globalforce.io

Overview

polleos is an GlobalForce smart contract for creating and managing decentralized polls. It supports both:

Files

Download pollgf.hpp
Download pollgf.cpp
Download pollgf.abi

Standard polls (one account = one vote)

Token-weighted polls (voting power based on user's token balance)

All polls, options, and votes are stored transparently on-chain, providing auditable, trustless community decision-making.

Features

Anyone can create polls with arbitrary text questions and custom voting options

Support for standard and token-weighted polls

Standard poll: each voter gets one vote

Token poll: each voter’s weight equals their balance of a specified token

Prevents double voting (each user can vote only once per poll)

All data on-chain: poll creation, vote casting, and tallies are persisted in GF tables

Actions

newpoll

Create a new, standard (non-token-weighted) poll.

newpoll(string question, account_name creator, vector<string> options)

question: The poll's question (string)
creator: Account paying for RAM (who creates the poll)
options: List of option strings

newtokenpoll

Create a new poll where vote weights are based on token balance.

newtokenpoll(string question, account_name payer, vector<string> options, extended_symbol token)

question: The poll's question
payer: Account paying for RAM (who creates the poll)
options: List of option strings
token: The token used to weight votes (contract and symbol)

vote

Vote in a poll.

vote(uint64_t poll_id, account_name voter, uint8_t option_id)

poll_id: ID of the poll
voter: Voter's account name
option_id: Index of the chosen option

How Voting Works

Each poll has an ID, question, and options (max 255).

Standard poll: Each voter can cast one vote for one option.
Token-weighted poll: Each voter can cast one vote; vote's weight = balance of specified token at voting time.
Users cannot vote more than once in the same poll (double-voting is rejected).

Data Tables

poll (scope: contract): Stores all poll definitions (ID, question, options, results, token info).
votes (scope: voter): Stores which poll(s) a user has voted in and for which option.
All votes and poll results are available for public audit.

Example Usage

Create a standard poll:

cleos push action polleosacc newpoll '["Who is best?", "alice", ["Alice", "Bob", "Charlie"]]' -p alice

Create a token-weighted poll:

cleos push action polleosacc newtokenpoll '["Favorite EOSIO wallet?", "bob", ["Anchor", "Scatter"], ["4,EOS","eosio.token"]]' -p bob

Vote in poll #1, option 2, as alice:

cleos push action polleosacc vote '[1, "alice", 2]' -p alice

Security & Best Practices

Each user can vote only once per poll (enforced on-chain).
For token-weighted polls, the user's token balance at the time of voting is used.
Only tokens with a valid contract and symbol can be used.
All results and tallies are on-chain for auditability.

Requirements GlobalForce/EOSIO/Antelope blockchain Standard eosio.token contract deployed for token-weighted polls

Extending

Add poll closing times and restrict voting after close
Allow poll creators to manage or remove polls
Add support for anonymous voting (via zero-knowledge or shielded tokens)
Integrate with UI or DAOs

License

MIT or similarly permissive license.

pollgf.hpp

#pragma once

#include <eosiolib/eosio.hpp>
#include <cmath>
#include "eosio.token.hpp"

/**
 * @class pollgf
 * EOSIO voting contract, supporting normal and token-weighted polls.
 */
class pollgf : public eosio::contract {
   public:
      typedef uint64_t                 poll_id_t;      // Type for poll IDs
      typedef std::vector<std::string> option_names_t; // List of option strings
      typedef eosio::extended_symbol   token_info_t;   // Token info (symbol+contract)
      typedef uint8_t                  option_id_t;    // Option index type

      pollgf(account_name contract_name)
         : eosio::contract(contract_name), _polls(contract_name, contract_name) {}

      /**
       * @struct option
       * Stores the name of a voting option.
       */
      struct option {
         std::string name;

         option(std::string name) : name(name) {}
         option() {}

         EOSLIB_SERIALIZE(option, (name))
      };

      /**
       * @struct option_result
       * Extends option by also tracking the number of votes.
       */
      struct option_result : option {
         double votes = 0; // Vote total (can be fractional for token-weighted polls)

         option_result(const std::string& name, uint64_t votes) : option(name), votes(votes) {}
         option_result(const std::string& name) : option_result(name, 0) {}
         option_result() {}

         EOSLIB_SERIALIZE(option_result, (name)(votes))
      };

      typedef std::vector<option_result> option_results;

      /**
       * @struct poll
       * Stores a poll (question, options, vote tallies, etc).
       */
      //@abi table
      struct poll {
         poll_id_t      id;            // Poll unique id
         std::string    question;      // Poll question text
         option_results results;       // Array of results (option name + vote tally)
         bool           is_token_poll = false; // True if poll is token-weighted
         token_info_t   token;         // Token info (if token-weighted)

         uint64_t primary_key() const { return id; }

         // Used for reverse order lookup/indexing (optional)
         uint64_t get_reverse_key() const { return ~id; }

         // Initializes poll object with all values and options.
         void set(poll_id_t id, const std::string& question,
                  const option_names_t& options, bool is_token_poll,
                  token_info_t token);

         EOSLIB_SERIALIZE(poll, (id)(question)(results)(is_token_poll)(token))
      };

      /**
       * @struct poll_vote
       * Stores a user's vote in a poll (per user per poll).
       */
      //@abi table votes
      struct poll_vote {
         poll_id_t   poll_id;    // The poll id this vote belongs to
         option_id_t option_id;  // Chosen option index

         uint64_t primary_key() const { return poll_id; }
         EOSLIB_SERIALIZE(poll_vote, (poll_id)(option_id))
      };

      // Table of polls, with a reverse index (not strictly needed)
      typedef eosio::multi_index<N(poll), poll,
         eosio::indexed_by<N(reverse),
            eosio::const_mem_fun<poll, uint64_t, &poll::get_reverse_key>
         >
      > poll_table;

      // Table of votes for each user (scope = user account)
      typedef eosio::multi_index<N(votes), poll_vote> vote_table;

      //@abi action
      void newpoll(const std::string& question, account_name creator,
                   const std::vector<std::string>& options);

      //@abi action
      void newtokenpoll(const std::string& question, account_name payer,
                        const std::vector<std::string>& options,
                        token_info_t token);

      //@abi action
      void vote(poll_id_t id, account_name voter, option_id_t option_id);

   private:
      // Stores poll on-chain.
      void store_poll(const std::string& question, account_name owner,
                      const option_names_t& options,
                      bool is_token_poll, token_info_t token);

      // Stores a user's vote and increments result.
      void store_vote(const poll& p, vote_table& votes, option_id_t option_id, double weight);

      // Stores a user's vote, using their token balance as weight.
      void store_token_vote(const poll& p, vote_table& votes, option_id_t option_id);

      // Converts an EOSIO asset to a weight (as a floating-point number)
      double to_weight(const eosio::asset& stake) {
         return stake.amount / std::pow(10, stake.symbol.precision());
      }

      poll_table _polls; // Main on-chain poll storage table
};

pollgf.cpp

/**
 * pollgf - A simple on-chain voting (poll) smart contract for EOSIO.
 * 
 * This contract allows anyone to create polls with multiple options, and for users to vote.
 * Polls can be standard (1 account = 1 vote) or "token-weighted" (vote weight is based on user's token balance).
 * Votes and poll data are stored on-chain for auditability and transparency.
 */

#include "pollgf.hpp"
#include <limits>

/**
 * @brief Sets up a poll object with the specified options and properties.
 * @param id            The poll's unique ID.
 * @param question      The poll question.
 * @param options       The list of options for voting.
 * @param is_token_poll True if this is a token-weighted poll.
 * @param token         Information about the token for weighting, if needed.
 */
void pollgf::poll::set(pollgf::poll_id_t id, const std::string& question,
                        const option_names_t& options, bool is_token_poll,
                        token_info_t token) {

   eosio_assert(!question.empty(), "Question can't be empty");

   this->id            = id;
   this->question      = question;
   this->is_token_poll = is_token_poll;
   this->token         = token;

   // Prepare results array for each voting option.
   results.resize(options.size());
   std::transform(options.begin(), options.end(), results.begin(),
                  [&](std::string str) {
                     eosio_assert(!str.empty(), "Option names can't be empty");
                     return option_result(str);
                  });
}

/**
 * @brief Stores a new poll in the contract's poll table.
 * @param question      The poll question.
 * @param poll_owner    Who pays RAM for this poll (creator).
 * @param options       The list of voting options.
 * @param is_token_poll Whether the poll is token-weighted.
 * @param token         Token info, if needed.
 */
void pollgf::store_poll(const std::string& question, account_name poll_owner,
                         const option_names_t& options,
                         bool is_token_poll, token_info_t token) {

   poll_id_t  id;

   eosio_assert(options.size() < std::numeric_limits<option_id_t>::max(),
                "Too many options");

   _polls.emplace(poll_owner, [&](poll& p) {
      id = _polls.available_primary_key();
      p.set(id, question, options, is_token_poll, token);
   });

   eosio::print("Poll stored with id: ", id);
}

/**
 * @brief Stores a user's vote in a poll (with explicit vote weight).
 *        Also increments the selected option's result.
 * @param p         The poll object.
 * @param votes     The vote table for the voter.
 * @param option_id The selected option's ID.
 * @param weight    The weight of the vote (1 for normal, token balance for token polls).
 */
void pollgf::store_vote(const pollgf::poll& p, pollgf::vote_table& votes,
                         option_id_t option_id, double weight) {

   eosio_assert(weight > 0, "Vote weight cannot be less than 0. Contract logic issue");

   // Voter (votes.get_scope()) pays for RAM.
   votes.emplace(votes.get_scope(), [&](poll_vote& v) {
      v.poll_id    = p.id;
      v.option_id  = option_id;
   });

   _polls.modify(p, votes.get_scope(), [&](poll& p) {
      p.results[option_id].votes += weight;
   });
}

/**
 * @brief Stores a user's token-weighted vote in a poll.
 *        Checks token balance, then records vote.
 * @param p         The poll object.
 * @param votes     The vote table for the voter.
 * @param option_id The selected option's ID.
 */
void pollgf::store_token_vote(const pollgf::poll& p, pollgf::vote_table& votes,
                               option_id_t option_id) {

   account_name voter = votes.get_scope();

   eosio::token token(p.token.contract);
   // Will fail if voter has no tokens
   eosio::asset balance = token.get_balance(voter, p.token.name());

   // Validate token balance
   eosio_assert(balance.is_valid(), "Balance of voter account is invalid. Something is wrong with token contract.");
   eosio_assert(balance.amount > 0, "Voter must have more than 0 tokens to participate in a poll!");

   // Store vote with token balance as weight
   store_vote(p, votes, option_id, to_weight(balance));
}

/**
 * @brief Create a new standard (non-token-weighted) poll.
 * @param question The poll question.
 * @param payer    Account paying for RAM.
 * @param options  The list of voting options.
 * @abi action
 */
void pollgf::newpoll(const std::string& question, account_name payer,
                      const option_names_t& options) {

   store_poll(question, payer, options, false, token_info_t());
}

/**
 * @brief Create a new token-weighted poll.
 * @param question   The poll question.
 * @param owner      Account paying for RAM.
 * @param options    The list of voting options.
 * @param token_inf  Info about the token to use for vote weighting.
 * @abi action
 */
void pollgf::newtokenpoll(const std::string& question, account_name owner,
                           const option_names_t& options, token_info_t token_inf) {

   eosio::token token(token_inf.contract);
   eosio_assert(token.exists(token_inf.name()), "This token does not exist");
   store_poll(question, owner, options, true, token_inf);
}

/**
 * @brief Cast a vote in a poll.
 *        Checks for double-voting and option validity, then stores the vote.
 * @param id        Poll id.
 * @param voter     Voter's account.
 * @param option_id Chosen option's index.
 * @abi action
 */
void pollgf::vote(pollgf::poll_id_t id, account_name voter, option_id_t option_id) {

   eosio::require_auth(voter);

   const poll & p = _polls.get(id, "Poll with this id does not exist");

   eosio_assert(option_id < p.results.size(), "Option with this id does not exist");

   vote_table votes(get_self(), voter);
   eosio_assert(votes.find(p.id) == votes.end(), "This account has already voted in this poll");

   if (p.is_token_poll)
      store_token_vote(p, votes, option_id);
   else
      store_vote(p, votes, option_id, 1);

   eosio::print("Vote stored!");
}

// Macro to register the contract's actions
EOSIO_ABI(pollgf, (newpoll)(newtokenpoll)(vote))

pollgf.abi

{
  "types": [{
      "new_type_name": "poll_id_t",
      "type": "uint64"
    },{
      "new_type_name": "option_results",
      "type": "option_result[]"
    },{
      "new_type_name": "token_info_t",
      "type": "extended_symbol"
    },{
      "new_type_name": "symbol_name",
      "type": "symbol"
    }
  ],
  "structs": [{
      "name": "option",
      "base": "",
      "fields": [{
          "name": "name",
          "type": "string"
        }
      ]
    },{
      "name": "option_result",
      "base": "option",
      "fields": [{
          "name": "votes",
          "type": "float64"
        }
      ]
    },{
      "name": "symbol_type",
      "base": "",
      "fields": [{
          "name": "value",
          "type": "symbol_name"
        }
      ]
    },{
      "name": "extended_symbol",
      "base": "symbol_type",
      "fields": [{
          "name": "contract",
          "type": "name"
        }
      ]
    },{
      "name": "poll",
      "base": "",
      "fields": [{
          "name": "id",
          "type": "poll_id_t"
        },{
          "name": "question",
          "type": "string"
        },{
          "name": "results",
          "type": "option_results"
        },{
          "name": "is_token_poll",
          "type": "bool"
        },{
          "name": "token",
          "type": "token_info_t"
        }
      ]
    },{
      "name": "poll_vote",
      "base": "",
      "fields": [{
          "name": "pollid",
          "type": "poll_id_t"
        },{
          "name": "optionid",
          "type": "uint8"
        }
      ]
    },{
      "name": "newpoll",
      "base": "",
      "fields": [{
          "name": "question",
          "type": "string"
        },{
          "name": "creator",
          "type": "name"
        },{
          "name": "options",
          "type": "string[]"
        }
      ]
    },{
      "name": "newtokenpoll",
      "base": "",
      "fields": [{
          "name": "question",
          "type": "string"
        },{
          "name": "creator",
          "type": "name"
        },{
          "name": "options",
          "type": "string[]"
        },{
          "name": "token",
          "type": "token_info_t"
        }
      ]
    },{
      "name": "vote",
      "base": "",
      "fields": [{
          "name": "id",
          "type": "poll_id_t"
        },{
          "name": "voter",
          "type": "name"
        },{
          "name": "option_id",
          "type": "uint8"
        }
      ]
    }
  ],
  "actions": [{
      "name": "newpoll",
      "type": "newpoll",
      "ricardian_contract": ""
    },{
      "name": "newtokenpoll",
      "type": "newtokenpoll",
      "ricardian_contract": ""
    },{
      "name": "vote",
      "type": "vote",
      "ricardian_contract": ""
    }
  ],
  "tables": [{
      "name": "poll",
      "index_type": "i64",
      "key_names": [
        "id"
      ],
      "key_types": [
        "poll_id_t"
      ],
      "type": "poll"
    },{
      "name": "votes",
      "index_type": "i64",
      "key_names": [
        "id"
      ],
      "key_types": [
        "poll_id_t"
      ],
      "type": "poll_vote"
    }
  ],
  "ricardian_clauses": []
}

On-chain Poll & Voting Smart Contract for GlobalForce

{% note alert %} This smart contract is presented here as an example, do not use it for production without prior verification. {% endnote %} If you don't have a

Loading...