Skip to content

Commit

Permalink
docs and readme
Browse files Browse the repository at this point in the history
  • Loading branch information
SkymanOne committed Aug 2, 2022
1 parent ab7b545 commit 99eb919
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 31 deletions.
70 changes: 61 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,41 @@
# Vote Pray Love
[![Check Set-Up & Build](https://github.com/SkymanOne/vote-pray-love/actions/workflows/check.yml/badge.svg)](https://github.com/SkymanOne/vote-pray-love/actions/workflows/check.yml)

Polkadot Blockchain Academy 2022 Cohort Final Exam Project.

## Milestones
- [x] Basic quadratic voting system
- [x] Anonymous quadratic system (commit-reveal)
- [x] Staking
- [x] Slashing
- [x] Caching-out
- [ ] Tests & Mocks
## Summary
This is a pallet quadratic voting pallet based off substrate [collective pallet](https://marketplace.substrate.io/pallets/pallet-collective/).
The additional feature is the slashing mechanism which provides an incentive for voters to collaborate in the decision making process.

**NOTE**: *This is an experimental pallet, no research has not conducted to actually prove economic costs of this governance system.*

## Potential idea for anonymous voting
## Idea

The idea behind is to introduce the quadratic voting, make it anonymous and slash-reward the voters. Let's break it down.
* Any account with identity can join a voting council to be a part of governance
* When the user joins the council, fixed amount of voting tokens is allocated the account
* The user must reserve some currency to have skin in a game
* When the proposal is created, the length in blocks is specified
* The voters submits votes anonymously. The votes are measured on a quadratic scale
* When the voting is over, the reveal phase begins
* Voters have limited time to reveal their actual votes
* Votes are calculated and the result is deduced
* If the voter is in minority (i.e. on the losing side). 10% of their stake is slashed and deposited to the *"pot"*
* If the voter is in majority, they receive even proportion of the reward from the *"pot"*
* If the vote is a tie, both parties get slashed and the money go to proposer
* Once the voter has finished all proposal, they can leave the organisation and *cash out*

## Motivation
The classical voting system has very little incentive for voters. The average vote turn-up does not exceed 10%. These ratio can not provide the true representation of opinion of the population. Therefore, the additional incentive has to be provided. I was interested in running this sort of system as a game theory experiment to see
how voters behave under such incentive.

Anonymous voting ensures that voters make choice motivated by personal choice and rationally and not profit-seeking.
The slashing ans reward system incentives voters to actually participate in the voting process while quadratic voting system has benefits of allowing voters to express strong preference of their choice. Quadratic voting has a linear dependency and not tied to the economic influence of particular entity.
![](assets/q-voting.png)

Different solution for anonymous voting have been considered. Here is the summary of the main two:

### Shared public key

1. Author creates a proposal and generates a keypair for this proposal
2. The public key of the proposer is distributed to voters
Expand All @@ -22,12 +47,39 @@ If the proposer reveals results before the timeout -> slashing
If the proposer tries to inside-trade the intermediate vote results -> no solution, might be worth using nominating random voters
to generate keypairs and use multi-sigs to collectively reveal the results

## Potential idea for anonymous voting 2
### Commit and Reveal
1. Author publishes a proposal
2. The voters commit their decision
3. Once timeout is out, the voters have some time to reveal their choices
4. If the voters does not reveal their results -> slashing

While the first approach may seem more convenient for the voter since they only need to submit a single transaction to represent a vote, it harm the global integrity of a governance protocol. The voter can simply inside-trade the actual votes and give away votes of other voters before the end of voting phase. The *commit and reveal* approach ensures trustlessness of a solution, hence, a suitable solution.

While it may seem a burden for user and proposer, the economic incentive actually motivates any voter to end the *commit* phase and *reveal* phase ASAP to potentially collect reward from voting.

## Objectives
A summary of what has been implemented and what's planned:
- [x] Basic quadratic voting system
- [x] Anonymous quadratic system (commit-reveal)
- [x] Staking
- [x] Slashing
- [x] Caching-out
- [x] Docs
- [ ] Tests & Mocks

## Running
Simply run script `build-run.sh` script to build a chain in release mode and run a dev node.
Otherwise, `/scripts` contains additional scripts to run your chain in docker.

Refer to [substrate setup instructions](docs/substrate-setup.md) to start hacking

### Account commit signatures for voting
* Alice - Yes - salt: `10` - `f26b35c9565f76a01b06ac1dd80832027a59306de8ac31b1019dff75890ec76c3376fb11985323f75788750a9c9ec0e17fc26440a49914d00ff2b7ef2d1a588f`
* Bob - No - salt: `10` - `b67cec01fabdc17233dc1080f09c4ffb86d0f19077f6c4f601f951a7f5a851175d127bb64142a24074596e2496388a9f01b00cd3a9db21a458e76757deda8585`

[Gist for generating signatures](https://gist.github.com/SkymanOne/b74096c4845e0af69b17fefb25eabf92)

## Resources

[Quadratic voting](https://www.economist.com/interactive/2021/12/18/quadratic-voting)
[Commit - reveal](https://karl.tech/learning-solidity-part-2-voting/)
Binary file added assets/q-voting.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 38 additions & 21 deletions pallets/slashing-voting/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,9 @@ pub mod pallet {
pub type ProposalIndex = u32;
pub type VoteToken = u8;

/// Shorted type for extracting current balance of a user
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;

// type ProposalOf<T> = Box<
// Proposal<<T as frame_system::Config>::AccountId, <T as frame_system::Config>::BlockNumber>,
// >;

pub trait IdentityProvider<AccountId> {
fn check_existence(account: &AccountId) -> bool;
}
Expand Down Expand Up @@ -180,23 +176,21 @@ pub mod pallet {
#[pallet::generate_store(pub(super) trait Store)]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);


/// Collection of all proposals hashes
#[pallet::storage]
pub type Proposals<T: Config> =
StorageValue<_, BoundedVec<T::Hash, T::MaxProposals>, ValueQuery>;

/// The actual data of proposal
#[pallet::storage]
pub type ProposalData<T: Config> =
StorageMap<_, Identity, T::Hash, Proposal<T::AccountId, T::BlockNumber, BalanceOf<T>>>;

/// The list of council member with their voting tokens
#[pallet::storage]
pub type Members<T: Config> = CountedStorageMap<_, Identity, T::AccountId, VoteToken, ValueQuery>;

/// Vote commits submitted by voters
#[pallet::storage]
pub type Commits<T: Config> =
StorageDoubleMap<_, Identity, T::AccountId, Identity, T::Hash, Commit<T::Signature>>;

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}

Expand All @@ -213,7 +207,7 @@ pub mod pallet {
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig {
fn build(&self) {
// Create Treasury account
// Create pot account
let account_id = <Pallet<T>>::account_id();
let min = T::Currency::minimum_balance();
if T::Currency::free_balance(&account_id) < min {
Expand Down Expand Up @@ -259,6 +253,7 @@ pub mod pallet {
//check if signer has identity | tested
ensure!(T::IdentityProvider::check_existence(&signer), Error::<T>::NoIdentity);

// ensure that user is not in the middle of voting process
let active_votes = <Commits<T>>::iter_prefix_values(signer.clone()).count();
ensure!(active_votes == 0, Error::<T>::InMotion);

Expand Down Expand Up @@ -290,24 +285,29 @@ pub mod pallet {
//check if signer is a member already | tested
ensure!(Self::is_member(&signer), Error::<T>::NotMember);

// ensure that we don't have too many proposal
let length_res = <Proposals<T>>::decode_len();
if let Some(length) = length_res {
if length == T::MaxProposals::get() as usize {
ensure!(false, Error::<T>::TooManyProposals);
}
}

// ensure that proposal exists
let proposal_hash = T::Hashing::hash_of(&proposal_text);
let (exist, _) = Self::proposal_exist(&proposal_hash);
ensure!(!exist, Error::<T>::DuplicateProposal);

// try to append, if error happens, this is probably we have too many proposals
ensure!(
<Proposals<T>>::try_append(proposal_hash).is_ok(),
Error::<T>::TooManyProposals
);

// calculate the end block of proposal
let end = duration + frame_system::Pallet::<T>::block_number();

// construct the proposal object
let proposal = Proposal {
title: *proposal_text,
proposer: signer.clone(),
Expand All @@ -321,7 +321,6 @@ pub mod pallet {
};

<ProposalData<T>>::insert(proposal_hash, proposal);

Self::deposit_event(Event::<T>::Proposed { account: signer, proposal_hash });

Ok(())
Expand All @@ -335,15 +334,21 @@ pub mod pallet {
//check if signer is a member already | tested
ensure!(Self::is_member(&signer), Error::<T>::NotMember);

//ensure that proposal data exists
let proposal_data = <ProposalData<T>>::get(&proposal);
ensure!(proposal_data.is_some(), Error::<T>::ProposalMissing);

//if we are here, then we know that data exists and we can unwrap it
let mut proposal_data = proposal_data.unwrap();

// if reveal end is set, then we know that voting phase ended
ensure!(proposal_data.reveal_end.is_none(), Error::<T>::VoteAlreadyEnded);

//make sure that we don't close voting phase too early
let current_block = frame_system::Pallet::<T>::block_number();
ensure!(proposal_data.poll_end <= current_block, Error::<T>::TooEarly);

// set the end of reveal phase
let current_block = frame_system::Pallet::<T>::block_number();
proposal_data.reveal_end = Some(current_block + T::RevealLength::get());

Expand All @@ -360,10 +365,14 @@ pub mod pallet {
//check if signer is a member already | tested
ensure!(Self::is_member(&signer), Error::<T>::NotMember);

//ensure that proposal data exists
let proposal_data = <ProposalData<T>>::get(&proposal);
ensure!(proposal_data.is_some(), Error::<T>::ProposalMissing);

//if we are here, then we know that data exists and we can unwrap it
let mut proposal_data = proposal_data.unwrap();

//if reveal phase end is not set, that means that we did not start it
ensure!(proposal_data.reveal_end.is_some(), Error::<T>::RevealNotStarted);

let reveal_end = proposal_data.reveal_end.unwrap();
Expand All @@ -376,7 +385,7 @@ pub mod pallet {
Self::deposit_votes(account, amount);
}

//deduce winning side
//deduce winning side, slash and reward voters
let result = proposal_data.ayes.cmp(&proposal_data.nays);
let pot_address = Self::account_id();
let amount: BalanceOf<T>;
Expand Down Expand Up @@ -424,6 +433,8 @@ pub mod pallet {
)?;
},
}

//set the amount that was slashed and paid
proposal_data.payout = amount;
<ProposalData<T>>::insert(&proposal, proposal_data);

Expand All @@ -444,7 +455,9 @@ pub mod pallet {
ensure!(commit.is_some(), Error::<T>::NoCommit);
let commit = commit.unwrap();

//get the data that supposed to be signed
let data = (vote.clone(), commit.salt).encode();
//and check signature validity
let valid_sign = commit.signature.verify(data.as_slice(), &signer);
ensure!(valid_sign, Error::<T>::SignatureInvalid);

Expand Down Expand Up @@ -479,7 +492,9 @@ pub mod pallet {
Vote::No => proposal_data.nays += commit.number as u32,
}

//push the vote counters
proposal_data.votes.push((signer.clone(), commit.number, vote));
//update the list of voters that revealed their choices
proposal_data.revealed.push(signer.clone());

<ProposalData<T>>::insert(proposal, proposal_data);
Expand All @@ -506,24 +521,26 @@ pub mod pallet {
ensure!(false, Error::<T>::InvalidArgument);
}

//make sure that vote has not been committed before
let committed = Self::already_committed_and_exist(&signer, &proposal);
ensure!(!committed, Error::<T>::DuplicateVote);

//ensure that proposal data exists
let proposal_data = <ProposalData<T>>::get(&proposal);
ensure!(proposal_data.is_some(), Error::<T>::ProposalMissing);
let proposal_data = proposal_data.unwrap();

//ensure that we don't commit to finished proposal
let current_block = frame_system::Pallet::<T>::block_number();
ensure!(current_block < proposal_data.poll_end, Error::<T>::VoteEnded);

let mut tokens_to_take: u8 = number;
if number > 1 {
tokens_to_take = number.pow(2);
}

let enough_tokens = Self::decrease_votes(&signer, tokens_to_take);
//subtract voting tokens based on quadratic scale
//i.e. tokens=vote^2
//make sure that voter has enough voting tokens
let enough_tokens = Self::decrease_votes(&signer, number.pow(2));
ensure!(enough_tokens, Error::<T>::NotEnoughVotingTokens);

//create commit instance
let commit = Commit { signature: data, salt, number };
<Commits<T>>::insert(signer.clone(), proposal, commit);

Expand Down Expand Up @@ -620,7 +637,7 @@ impl<T: Config> Pallet<T> {
Ok(())
}

/// Intermediate
/// get voting pot address to deposit slashed tokens to and take rewards from
pub fn account_id() -> T::AccountId {
T::PalletId::get().into_account_truncating()
}
Expand Down
4 changes: 3 additions & 1 deletion pallets/slashing-voting/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ pub struct Proposal<AccountId, BlockNumber, Balance> {
/// The number of votes each voter gave
pub votes: Vec<(AccountId, u8, Vote)>,
/// Users who revealed their choices.
/// Allows to verify who did not reveal on time
/// Allows to verify who did not reveal on time.
/// This may look as data duplication, but it will reduce runtime
/// otherwise we need to parse `votes` vector and compose vector of required format
pub revealed: Vec<AccountId>,
/// The amount that was slashed and distributed
pub payout: Balance
Expand Down

0 comments on commit 99eb919

Please sign in to comment.