--- sip: 196 title: V3GM Election Module network: Other status: Draft type: Meta-Governance author: Andy T CF (@andytcf) implementor: Andy T CF (@andytcf) discussions-to: 'https://research.synthetix.io' created: 2021-12-13T00:00:00.000Z --- <!--You can leave these HTML comments in your merged SIP and delete the visible duplicate text guides, they will not appear and may be helpful to refer to if you edit it again. This is the suggested template for new SIPs. Note that an SIP number will be assigned by an editor. When opening a pull request to submit your SIP, please use an abbreviated title in the filename, `sip-draft_title_abbrev.md`. The title should be 44 characters or less.--> ## Simple Summary <!--"If you can't explain it simply, you don't understand it well enough." Simply describe the outcome the proposed changes intends to achieve. This should be non-technical and accessible to a casual community member.--> This SIP introduces one of the first components of the V3 Governance Module - called the Election Module. The Election Module is proposed to replace the existing [Snapshot](http://docs.snapshot.page/) election process carried out by the Synthetix DAOs whilst also providing a standard module which other protocols can use for their governance system. ## Abstract <!--A short (~200 word) description of the proposed change, the abstract should clearly describe the proposed change. This is what *will* be done if the SIP is implemented, not *why* it should be done or *how* it will be done. If the SIP proposes deploying a new contract, write, "we propose to deploy a new contract that will do x".--> If this meta-governance proposal is approved, the Election Module will replace the off-chain elections carried out on Snapshot (i.e [example election space](https://snapshot.org/#/spartancouncil.eth)) - by deploying, on Optimism, a modular on-chain solution which will be configured to the existing governance election standards of Synthetix. ## Motivation <!--This is the problem statement. This is the *why* of the SIP. It should clearly explain *why* the current state of the protocol is inadequate. It is critical that you explain *why* the change is needed, if the SIP proposes changing how something is calculated, you must address *why* the current calculation is innaccurate or wrong. This is not the place to describe how the SIP will address the issue!--> Since the introduction of [SIP-93 - Delegated Council Governance](https://sips.synthetix.io/sips/sip-93/) which mainly introduced the *Spartan Council* the Synthetix Governance has evolved. In the Synthetix Governance System - there are currently 5 official bodies - Spartan Council - Ambassador Council - Grants Council - Treasury Council - Core Contributor Committee In the journey to this current stage - the Synthetix Governance system has relied heavily on using apps that are still constantly improving (i.e Snapshot, Synthetix Staking dApp). As the protocol governance grew, the protocol started out-growing existing processes, identifying the need to start building a system surrounding the current and future plans of Synthetix Governance - leading to the catalyst behind the V3GM (V3 Governance Module) product squad within the Core Contributors. One of the first things that the protocol needed to solve was removing the centralized aspect of the governing body elections - the Election Module was built to replicate most of the election process that happens on snapshot into an modular and customizable on-chain solution. More specifically - the election module addresses: - Centralization of election strategy updating (both on snapshot team and synthetix cc's) - Dependency on Core Contributors collecting nominations on Discord - Dependency on Core Contributor/pDAO in transferring the Council/Committee NFTs on the conclusion of an election - Standardise elections within the Web 3.0 space so other projects can replicate delegated governance ## Specification <!--The specification should describe the syntax and semantics of any new feature, there are five sections 1. Overview 2. Rationale 3. Technical Specification 4. Test Cases 5. Configurable Values --> ### Overview <!--This is a high level overview of *how* the SIP will solve the problem. The overview should clearly describe how the new feature will be implemented.--> The Election Module is a factory set of smart contracts that given parameters relating to the delegated group it represents (i.e seat number, epoch length) will enable the necessary election processes to occur on-chain in a modular and decentralized way. ### Rationale <!--This is where you explain the reasoning behind how you propose to solve the problem. Why did you propose to implement the change in this way, what were the considerations and trade-offs. The rationale fleshes out what motivated the design and why particular design decisions were made. It should describe alternate designs that were considered and related work. The rationale may also provide evidence of consensus within the community, and should discuss important objections or concerns raised during discussion.--> In the process of the V3GM - Election Module R&D phase, the following design decisions were considered - On-chain vs Off-chain - L1 vs L2 - NFT Shuffling - Voting Strategies - Accountability - Scheduling - Prevent gas limit reverts - Ties exceding seats count - Module Initialization (epoch 0) - Dismissal & Emergency Election #### On-Chain Vs Off-Chain The existing election process happens off-chain on Snapshot - one of the main problems that arose due to this was the amount of discretion and centralization that was present in one of the most important meta-governance processes. When an election was concluded - the community had to trust the protocolDAO in replicating the results of the elections on-chain via the transferring of NFTs. With the election module being on-chain all of this can be automated based on non-contestable on-chain data. #### L1 vs L2 With the increased usage of Ethereum L1, the cost of governance if it was to exist purely on L1 will be a limiting factor to how reliable the governance framework would be. To find the balance between on-chain governance whilst keeping the cost of governance low - the Optimism Network was selected. #### NFT Shuffling When the election is resolved, the previous council member NFTs are burned and a new set is minted for the new council members. #### Voting Strategies There are many different Voting Methods and alternatives to choose from when implementing an election system. In order to meet the needs and demands of different bodies or organizations the system relies on the flexibility to choose or implemet different strategies for different areas: - **Ballot Conformation**: What the voter is allowed to vote and how it's interpreted. i.e. single candidate, unordered list of options, ranked list of candidates, etc. - **Vote Counting**: This may condition the other strategies and refers to how the votes are counted to identify the winner or winners. i.e. Plurality, STV, Condorcet winner, Borda's winner, etc. - **Voting Unit**: Defines how an address voting uints are calculated. It depends on one or several variables (balance, debt share, etc.). The strategy could contain any arbitrary code that performs any arbitrary DeFi calculation, and produces the number of voting units a particular address has. - **Voting Power**: That defines the voting power of each voter. It takes the Voting Units and filter it to identify an address voting power. I.e. linear, quadratic, etc. Note that some strategies will condition the options for the rest of the areas. **Strategy Implemented** In order to have a limited scope, this SIP will allow for different strategies while only implementing a direct replacement of V2 voting strategy. This means, the following considerations are taken for the Voting Strategy: - **Ballot Conformation**: The implementation will support a single candidate option. - **Vote Counting**: The strategy to implement is *Plurality*. The Voting Power received from voters is added to the voted candidate(s). The most voted candidates are elected to be the new members. - **Voting Unit**: Weighted debt at the beginning of the election phase. - **Voting Power**: Linear. 1 debt unit = 1 vote. #### Transparency and Verification The election data must be accessible to audit and challenge the results. All election related actions should emit events. Candidates, Ballots, voters and results must be available. #### Scheduling The scheduling indicates the length of the epoch and the time allowed for the different periods (nomination, voting, evaluation). The system is designed so that the current epoch length and current vote periods are not modifiable when they are scheduled. The periods defined are: - Idle Period: Optional period at the beginning of the epoch, where past elections just ended and next elections haven't begun. - Nomination Period: during this stage the only available action for the election of the next council is nominations. Addresses can self nominate (or withdraw their nomination). The result of this period is a fixed list of candidates. - Voting Period: during this stage the only available action for the election of the next council is elect (voting). Addresses (voters) cast votes for the ordered list of candidates. It is possible to re-cast a vote to change the candiates voted, the latest casted vote is the only one that takes effect. The result of this period is a set of ballots and the accumulated voting power for each. - Evaluation Period: This period is fixed so that the election can be evaluated (votes counted) and resolved (new council is formed). The election is evaluated by batches (a small election may fit in only one batch) using one tx per batch to prevent gas exhaustion, once the election is evaluated, the elecion is resolved in a separate tx. Each period defines which action can be executed related to the election of the next council conformation. ⚠️ There will be an owner restricted option to invoke an emergency election in order to shorter the current epoch in case its duration is wrongly set too large (i.e. 900 days instead of 90) ⚠️ The image below shows the relation between the different epochs and the periods. ![](https://i.imgur.com/Iv4TxJ5.png) During Epoch N the election of Epoch N+1 is executed. The three periods relate to the end of the current epoch (T), so - the Evaluation Period spawns from `(T - evaluationPeriod)` to `(T)`, - the Voting Period spawns from `(T - evaluationPeriod - votingPeriod)` to `(T - evaluationPeriod)`, - and the Nomination Period spawns from `(T - evaluationPeriod - votingPeriod - nominationPeriod)` to `(T - evaluationPeriod - votingPeriod)`. **Important Note**: the next epoch starts when the election is resolved (success call to `resolveElection()`), at which a new set of NFTs is distributed to the new council members. At that time all Next Epoch parameters become the current Epoch parameters. #### Prevent gas limit reverts Given the number of candidates and votes is not limited, processing or accessing them can lead to large arrays processing which can cause out-of-gas execution errors. To prevent this, all functions that need to iterate over such arrays need to implement pagination. All view functions for ballots or candidates should implement pagination as well. Public/protected functions that need to process data looping over the candidates or ballots need to implement an iteration process. The following piece of code shows an example of the `evaluateElection` process. It relies on a boolean flag that can be retrieved with `isElectionEvaluated()` that indicates if the process finished and the `evaluateElectionBatch()` function that will use an internal property to identify the latest item processed. The batch size can be fine tunned using the setter `setMaxProcessingBatchSize()`. ``` let finished = await ElectionModule.isElectionEvaluated(); while (!finished) { await (await ElectionModule.evaluateElectionBatch()).wait(); finished = await ElectionModule.isElectionEvaluated(); } ``` #### Ties exceding seats count An election can result in a tie for a set of the candidates. If there are seats available for all of them, that is not an issue since all council members are pairs. The real issue appears when there's no room in the seats available for the tied set of candidates, and, in that situation, the election module should have a mechanism in place to pick the right candidates. For that edge case, the solution proposed in the SIP is to let the contract pick the candidate based on how the counting strategy finds the matches, allowing it to grant the seats to the first matches identified. If later (after exposing the ElectionModule to real life elections) there's a need to get another kind of solution, it will be considered in another SIP. #### Module Initialization (epoch 0) When the ElectionModule is first initialized, it only has a single council member, the owner of the system that hosts the ElectionModule. As such, the first epoch is "fast forwarded" immediately to the nomination phase for the second epoch. Such fast forward mechanism is considered an emergency election state. #### Dismissal & Emergency Election Community members may call dismiss(address[] members) to use their voting power to attempt to dismiss one or more council members. Only those who have voted for a specific address can dismiss such address. Voting power is counted with a dismissal strategy (similar to the election strategy) and for every member that meets the dismissalThresholdPercent (which is a percent of the total supply), the member is automatically dismissed and its NFT burnt. If dismissals produce a number of active council members beneath minimumActiveSeats, the ElectionModule enters an emergency election state, which is identical to the state that the module is in in its first epoch. I.e. the epoch is fast forwarded to the nomination phase for the next epoch. If all council members are dismissed during such an event, then the owner is re-assigned as the only council member. Note that the owner can be an EOA, a multisig, or a future V3GM GovernorModule ### Technical Specification <!--The technical specification should outline the public API of the changes proposed. That is, changes to any of the interfaces Synthetix currently exposes or the creations of new ones.--> ***ElectionData Structure*** ElectionData is consumed/populated per election. *All properties have private accessibility unless marked otherwise, different strategies might need other data structures or meaning.* | name | type | description | | -------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- | | ballotIds | bytes32[] | Array used to keep track of all ballot ids (a ballotId is the hash of the candidates array voted in the ballot) | | ballotCandidates | mapping(bytes32 => address[]) | Mapping of ballot's candidates list (in the same order as received) indexed by ballot id. | | ballotTotalVotes | mapping(bytes32 => uint256) | Mapping of votes received by ballot indexed by ballot id. | | addressBallotVote | mapping(address => bytes32) | Mapping of votes casted by voter indexed by voter address. It returns the ballotId. | | addressVotePower | mapping(address => uint256) | Mapping of vote power of the voter indexed by voter address. | | addressHasVoted | mapping(address -> bool) | Mapping used to check if a particular address has voted for the current election. | | candidates | address[] | Array used to keep track of the candidates for the election. | | candidatesPosition | mapping(address -> uint) | Position of an address on the candidates Array. Note that position 1 corresponds to index 0. | | electionProcessedBatchIndex | uint256 | Auxiliar index used in batched election evaluation. Points to the latest candidate processed. | | isElectionEvaluated | boolean | Flag to indicate whether an election has been evaluated. | | dismissalProcessedBatchIndex | uint256 | Auxiliar index used in batched dismissal evaluation. Points to the latest candidate processed. | | isDismissalEvaluated | boolean | Flag to indicate whether an dismissal has been evaluated. | | nextEpochMembers | address[] | Used to keep track of the next epoch's Members. | | nextEpochMemberVotes | uint[] | Used to keep track of the next epoch's nextEpochMembers votes ***This depends on the voting strategy*** | | candidateVotes | mapping(address -> uint) | Number of votes a candidate has for being a council member in the next epoch. ***This depends on the voting strategy*** | **Properties** *All properties have private accessibility unless marked otherwise* | name | type | description | | ---------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | memberTokenAddress | ERC721 | NFT token awarded to council members in the current epoch. It can be taken away by demotion at any moment, or by election in the next epoch. | | electionData | mapping(uint => ElectionData) | Mapping used to keep track of Voting data for the current and past elections | | latestElectionDataIndex | uint | Index pointing to the latest electionData | | members | address[] | Mapping used to keep track of the current council members. | | seatCount | uint | Number of members in the current epoch. | | nextSeatCount | uint | Number of members in the next epoch. | | epochStart | uint | Start reference of the current epoch. | | epochDuration | uint | Duration of the current epoch. | | nextEpochDuration | uint | Duration of the next epoch. | | nominationPeriodDuration | uint | Duration of the nomination period in which only nominations are allowed. | | nextNominationPeriodDuration | uint | Duration of the next nomination period in which only nominations are allowed. | | votingPeriodDuration | uint | Duration of the voting period in which only voting are allowed. | | nextVotingPeriodDuration | uint | Duration of the next nomination period in which only voting are allowed. | | evaluationPeriodDuration | uint | Duration of the evaluation period in which only the election evaluation and resolution can be done. | | nextEvaluationPeriodDuration | uint | Duration of the next nomination period in which only the election evaluation and resolution can be done. | | dismissal | address[] | Mapping used to keep track of the number of votes a nominee has for being a council member in the next epoch. | | dismissalVotes | mapping(address -> uint) | Mapping used to keep track of the number of votes a nominee has for being demoted in the current epoch. | | dismissalThreshold | uint | Quorum requirement for demoting a council member in % of total supply, snapshot at the beginning of the epoch. | | minimumSeatCountThreshold | uint | Number of active council members needed (which can be truncated via demotion) for the activation of an emergency election. | | emergencyElectionDuration | uint | Duration of an emergency election. When an emergency election triggers, the remaining time of the current epoch is reduced to this duration. | | maxProcessingBatchSize | uint | Used to limit the batch size. This parameter is maintained on epochs changes | | countingStrategy | CountingStrategy | Method for counting votes for election and demotion if more than one is available in the configuration. ***This depends on the voting strategies implemented*** | **View Functions** | name | return type | description | | ------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------ | | getMemberToken | address | Returns the address of the member token (i.e 0x..) | | getCandidates | address[] | Gets an array of addresses of those who have nominated themselves for the current epoch. ***It needs to support pagination*** | | getBallotIds | bytes32[] | Gets an array of ballot ids. ***It needs to support pagination*** | | getBallotAddresses | address[] | Gets one or an array of ballots, each containing the array of addresses that contains the ballot for a given ballotId or list of ballotIds| | getBallotsVotes | uint[] | Gets the array of votes received for a given list of ballotIds. | | getVoterBallot | address[] | Gets the ballot (as array of addresses) voted for a given address. | | getVoterVotePower | uint | Gets the vote power for a given address. | | getMembers | address[] | Gets an array of addresses of those the council members | | getSeatCount | uint | Gets the seats count of current epoch | | isNominating | boolean | true if in nomination period | | isVoting | boolean | true if in voting period | | isEvaluating | boolean | true if in evaluation period | | isElectionEvaluated | boolean | Indicates if the election evaluation is finished | | isDismissalEvaluated | boolean | Indicates if the dismissal evaluation is finished | **Public Functions** | name | parameters | accessibility | description | | ------------------ | ---------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------ | | nominate | null | external | Adds msg.sender to candidates. Reverts if address is already in the array. | | withdrawNomination | null | external | Removes msg.sender from candidates. Reverts if address is not in the array. | | elect | address[] | external | Cast vote for a candidate or list of candidates to become a council member in the next epoch. Reverts if the user has no, or no more, voting power. | | evaluateElectionBatch | null | public | Counts votes and returns the member array for the next epoch. | | resolveElection | null | public | Checks if election was evaluated and performs state changes. | | dismiss | address[] | external | Counts votes for a dismissal. | | evaluateDismissalBatch | null | public | Counts votes and returns the array of members to be demoted in the current epoch. | | resolveDismissal | null | public | Checks if the dismissal was evaluated and perform the state changes. | **Restricted Functions (onlyOwner)** **IMPORTANT**: Initially on MVP the owner will be the pDAO as a backstop - eventually the councils themselves (GovernorModule.sol to be explored in another SIP) will be the owner of their relevant ElectionModule, via a V3GM GovernorModule. | name | parameters | accessibility | description | | ------------------- | ---------- | ------------- | -------------------- | | setNextSeatCount | uint | external | Changes the property `nextSeatCount` in state | | setNextEpochDuration | uint | external | Changes the property `nextEpochDuation` in state | | setNextEpochNominationPeriod | uint | external | Changes the property `nextEpochNominationPeriod` in state | | setNextEpochVotingPeriod | uint | external | Changes the property `nextEpochVotingPeriod` in state | | setNextEpochEvaluationPeriod | uint | external | Changes the property `nextEpochEvaluationPeriod` in state | | setMaxProcessingBatchSize | uint | external | Changes the property `maxProcessingBatchSize` in state | | setCountingStrategy | uint | external | Changes the property `countingStrategy` in state | | upgradeMemberToken | address | external | Changes the `memberTokenAddress` in state | **Events** | name | parameters | description | | ---------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | MemberTokenCreated | address memberToken | MemberToken Created at `memberToken` | MemberTokenSet | address memberToken | MemberToken Set to `memberToken` | Nominate | address candidate | `candidate` nominated | | WithdrawNomination | address candidate| `candidate` withdraw nomination | | Elect | address[] candidates, address elector, uint votePower | `candidates` voted by `elector` with `votePower` | | WithdrawVote | address[] candidates, address elector, uint votePower | `elector` withdraw `votePower` from `candidates`. This event is the result of `elector` re-casting a vote with a new set of candidates | | ElectionBatchEvaluated | boolean isLast | Election batch was evaluated. If it was the latest (electionIsEvaluated) `isLast` is `true` | | ElectionResolved | address[] members| Election was resolved. New NFTs distributed to `members` | | Dismiss | address[] candidates, address elector, uint votePower | `candidates` voted for dismissal by `elector` with `votePower` | | DismissalBatchEvaluated | boolean isLast | Dismissal batch was evaluated. If it was the latest (dismissalIsEvaluated) `isLast` is `true` | | DismissalResolved | address[] members| Dismissal was resolved. NFTs removed (burned) for `members` | ### Test Cases <!--Test cases for an implementation are mandatory for SIPs but can be included with the implementation..--> 1. Standard Election 2. Standard Dismissal 3. Emergency Election ### Configurable Values (Via SCCP) <!--Please list all values configurable via SCCP under this implementation.--> - `memberToken` - `electionToken` - `nextSeatCount` - `nextEpochDuration` - `nextEpochNominationPeriod` - `nextEpochVotingPeriod` - `nextEpochEvaluationPeriod` - `dismissalThreshold` - `minimumSeatCountThreshold` - `emergencyElectionDuration` - `maxProcessingBatchSize` - `countingStrategy` ## Copyright Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).