diff --git a/README.md b/README.md index da853c1..216d479 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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/) diff --git a/assets/q-voting.png b/assets/q-voting.png new file mode 100644 index 0000000..0d57792 Binary files /dev/null and b/assets/q-voting.png differ diff --git a/pallets/slashing-voting/src/lib.rs b/pallets/slashing-voting/src/lib.rs index 619e6a6..e42c1ac 100644 --- a/pallets/slashing-voting/src/lib.rs +++ b/pallets/slashing-voting/src/lib.rs @@ -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 = <::Currency as Currency<::AccountId>>::Balance; - - // type ProposalOf = Box< - // Proposal<::AccountId, ::BlockNumber>, - // >; - pub trait IdentityProvider { fn check_existence(account: &AccountId) -> bool; } @@ -180,23 +176,21 @@ pub mod pallet { #[pallet::generate_store(pub(super) trait Store)] #[pallet::without_storage_info] pub struct Pallet(_); - - + /// Collection of all proposals hashes #[pallet::storage] pub type Proposals = StorageValue<_, BoundedVec, ValueQuery>; - + /// The actual data of proposal #[pallet::storage] pub type ProposalData = StorageMap<_, Identity, T::Hash, Proposal>>; - + /// The list of council member with their voting tokens #[pallet::storage] pub type Members = CountedStorageMap<_, Identity, T::AccountId, VoteToken, ValueQuery>; - + /// Vote commits submitted by voters #[pallet::storage] pub type Commits = StorageDoubleMap<_, Identity, T::AccountId, Identity, T::Hash, Commit>; - #[pallet::hooks] impl Hooks> for Pallet {} @@ -213,7 +207,7 @@ pub mod pallet { #[pallet::genesis_build] impl GenesisBuild for GenesisConfig { fn build(&self) { - // Create Treasury account + // Create pot account let account_id = >::account_id(); let min = T::Currency::minimum_balance(); if T::Currency::free_balance(&account_id) < min { @@ -259,6 +253,7 @@ pub mod pallet { //check if signer has identity | tested ensure!(T::IdentityProvider::check_existence(&signer), Error::::NoIdentity); + // ensure that user is not in the middle of voting process let active_votes = >::iter_prefix_values(signer.clone()).count(); ensure!(active_votes == 0, Error::::InMotion); @@ -290,6 +285,7 @@ pub mod pallet { //check if signer is a member already | tested ensure!(Self::is_member(&signer), Error::::NotMember); + // ensure that we don't have too many proposal let length_res = >::decode_len(); if let Some(length) = length_res { if length == T::MaxProposals::get() as usize { @@ -297,17 +293,21 @@ pub mod pallet { } } + // ensure that proposal exists let proposal_hash = T::Hashing::hash_of(&proposal_text); let (exist, _) = Self::proposal_exist(&proposal_hash); ensure!(!exist, Error::::DuplicateProposal); + // try to append, if error happens, this is probably we have too many proposals ensure!( >::try_append(proposal_hash).is_ok(), Error::::TooManyProposals ); + // calculate the end block of proposal let end = duration + frame_system::Pallet::::block_number(); + // construct the proposal object let proposal = Proposal { title: *proposal_text, proposer: signer.clone(), @@ -321,7 +321,6 @@ pub mod pallet { }; >::insert(proposal_hash, proposal); - Self::deposit_event(Event::::Proposed { account: signer, proposal_hash }); Ok(()) @@ -335,15 +334,21 @@ pub mod pallet { //check if signer is a member already | tested ensure!(Self::is_member(&signer), Error::::NotMember); + //ensure that proposal data exists let proposal_data = >::get(&proposal); ensure!(proposal_data.is_some(), Error::::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::::VoteAlreadyEnded); + //make sure that we don't close voting phase too early let current_block = frame_system::Pallet::::block_number(); ensure!(proposal_data.poll_end <= current_block, Error::::TooEarly); + // set the end of reveal phase let current_block = frame_system::Pallet::::block_number(); proposal_data.reveal_end = Some(current_block + T::RevealLength::get()); @@ -360,10 +365,14 @@ pub mod pallet { //check if signer is a member already | tested ensure!(Self::is_member(&signer), Error::::NotMember); + //ensure that proposal data exists let proposal_data = >::get(&proposal); ensure!(proposal_data.is_some(), Error::::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::::RevealNotStarted); let reveal_end = proposal_data.reveal_end.unwrap(); @@ -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; @@ -424,6 +433,8 @@ pub mod pallet { )?; }, } + + //set the amount that was slashed and paid proposal_data.payout = amount; >::insert(&proposal, proposal_data); @@ -444,7 +455,9 @@ pub mod pallet { ensure!(commit.is_some(), Error::::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::::SignatureInvalid); @@ -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()); >::insert(proposal, proposal_data); @@ -506,24 +521,26 @@ pub mod pallet { ensure!(false, Error::::InvalidArgument); } + //make sure that vote has not been committed before let committed = Self::already_committed_and_exist(&signer, &proposal); ensure!(!committed, Error::::DuplicateVote); + //ensure that proposal data exists let proposal_data = >::get(&proposal); ensure!(proposal_data.is_some(), Error::::ProposalMissing); let proposal_data = proposal_data.unwrap(); + //ensure that we don't commit to finished proposal let current_block = frame_system::Pallet::::block_number(); ensure!(current_block < proposal_data.poll_end, Error::::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::::NotEnoughVotingTokens); + //create commit instance let commit = Commit { signature: data, salt, number }; >::insert(signer.clone(), proposal, commit); @@ -620,7 +637,7 @@ impl Pallet { 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() } diff --git a/pallets/slashing-voting/src/types.rs b/pallets/slashing-voting/src/types.rs index 96cc052..ca508cd 100644 --- a/pallets/slashing-voting/src/types.rs +++ b/pallets/slashing-voting/src/types.rs @@ -28,7 +28,9 @@ pub struct Proposal { /// 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, /// The amount that was slashed and distributed pub payout: Balance