Repository: Synthetixio/synthetix-v3
Pull request #820 (2022-02-25 19:33:32)

Add missing subtask name

codecov[bot]: # [Codecov](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio) Report
> Merging [#820](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio) (127ab40) into [main](https://codecov.io/gh/Synthetixio/synthetix-v3/commit/b84484ed984582400ccaf5f1d26ad4ef77bdcb82?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio) (b84484e) will **increase** coverage by `0.96%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/graphs/tree.svg?width=650&height=150&src=pr&token=B9BK0U5KAT&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio)](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio)

```diff
@@            Coverage Diff             @@
##             main     #820      +/-   ##
==========================================
+ Coverage   98.42%   99.39%   +0.96%     
==========================================
  Files          77       31      -46     
  Lines        1272      330     -942     
  Branches       98       58      -40     
==========================================
- Hits         1252      328     -924     
+ Misses         20        2      -18     
```

| Flag | Coverage Δ | |
|---|---|---|
| core-contracts | `100.00% <ø> (ø)` | |
| core-js | `99.26% <ø> (ø)` | |
| core-modules | `?` | |
| deployer | `?` | |
| synthetix-main | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio) | Coverage Δ | |
|---|---|---|
| [...kages/core-modules/contracts/mixins/CommsMixin.sol](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvY29yZS1tb2R1bGVzL2NvbnRyYWN0cy9taXhpbnMvQ29tbXNNaXhpbi5zb2w=) | | |
| [packages/deployer/subtasks/deploy-contract.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvc3VidGFza3MvZGVwbG95LWNvbnRyYWN0Lmpz) | | |
| [packages/deployer/subtasks/clear-deployments.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvc3VidGFza3MvY2xlYXItZGVwbG95bWVudHMuanM=) | | |
| [packages/deployer/subtasks/sync-proxy.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvc3VidGFza3Mvc3luYy1wcm94eS5qcw==) | | |
| [...es/contracts/submodules/election/ElectionTally.sol](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvY29yZS1tb2R1bGVzL2NvbnRyYWN0cy9zdWJtb2R1bGVzL2VsZWN0aW9uL0VsZWN0aW9uVGFsbHkuc29s) | | |
| [packages/deployer/internal/abis.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvaW50ZXJuYWwvYWJpcy5qcw==) | | |
| [packages/deployer/internal/errors.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvaW50ZXJuYWwvZXJyb3JzLmpz) | | |
| [packages/deployer/subtasks/upgrade-proxy.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvc3VidGFza3MvdXBncmFkZS1wcm94eS5qcw==) | | |
| [packages/deployer/internal/contract-helper.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvaW50ZXJuYWwvY29udHJhY3QtaGVscGVyLmpz) | | |
| [packages/deployer/subtasks/deploy-contracts.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvc3VidGFza3MvZGVwbG95LWNvbnRyYWN0cy5qcw==) | | |
| ... and [36 more](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio). Last update [b84484e...127ab40](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/820?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio).

ajsantander: It's a total mystery to me that hardhat was fetching and executing the task anyway.

I just checked in main. The name is undefined, but it executes the correct task correctly nonetheless 🤷 

Patch:
diff --git a/packages/core-contracts/contracts/errors/ArrayError.sol b/packages/core-contracts/contracts/errors/ArrayError.sol
new file mode 100644
index 00000000..449ae16b
--- /dev/null
+++ b/packages/core-contracts/contracts/errors/ArrayError.sol
@@ -0,0 +1,6 @@
+//SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+library ArrayError {
+    error EmptyArray();
+}
diff --git a/packages/core-js/utils/ethers/events.js b/packages/core-js/utils/ethers/events.js
index 9b5143b4..15ef0632 100644
--- a/packages/core-js/utils/ethers/events.js
+++ b/packages/core-js/utils/ethers/events.js
@@ -15,7 +15,7 @@ function findEvent({ receipt, eventName, contract = undefined }) {
 
   events = events.filter((e) => e.event === eventName);
   if (!events || events.length === 0) {
-    throw new Error(`Cannot find event ${eventName} in receipt.`);
+    return undefined;
   }
 
   return events.length === 1 ? events[0] : events;
diff --git a/packages/core-modules/contracts/interfaces/IElectionModule.sol b/packages/core-modules/contracts/interfaces/IElectionModule.sol
index 1c72257b..b87427c0 100644
--- a/packages/core-modules/contracts/interfaces/IElectionModule.sol
+++ b/packages/core-modules/contracts/interfaces/IElectionModule.sol
@@ -9,6 +9,8 @@ interface IElectionModule {
     function initializeElectionModule(
         string memory councilTokenName,
         string memory councilTokenSymbol,
+        address[] memory firstCouncil,
+        uint8 minimumActiveMembers,
         uint64 nominationPeriodStartDate,
         uint64 votingPeriodStartDate,
         uint64 epochEndDate
@@ -46,6 +48,10 @@ interface IElectionModule {
 
     function setNextEpochSeatCount(uint8 newSeatCount) external;
 
+    function setMinimumActiveMembers(uint8 newMinimumActiveMembers) external;
+
+    function dismissMembers(address[] calldata members) external;
+
     // ---------------------------------------
     // User write functions
     // ---------------------------------------
@@ -54,7 +60,7 @@ interface IElectionModule {
 
     function withdrawNomination() external;
 
-    function elect(address[] calldata candidates) external;
+    function cast(address[] calldata candidates) external;
 
     function evaluate(uint numBallots) external;
 
@@ -82,6 +88,8 @@ interface IElectionModule {
 
     function getNextEpochSeatCount() external view returns (uint8);
 
+    function getMinimumActiveMembers() external view returns (uint8);
+
     // Epoch and periods
     // ~~~~~~~~~~~~~~~~~~
 
diff --git a/packages/core-modules/contracts/modules/ElectionModule.sol b/packages/core-modules/contracts/modules/ElectionModule.sol
index afddcc2e..fbbda9f7 100644
--- a/packages/core-modules/contracts/modules/ElectionModule.sol
+++ b/packages/core-modules/contracts/modules/ElectionModule.sol
@@ -25,18 +25,26 @@ contract ElectionModule is
     function initializeElectionModule(
         string memory councilTokenName,
         string memory councilTokenSymbol,
+        address[] memory firstCouncil,
+        uint8 minimumActiveMembers,
         uint64 nominationPeriodStartDate,
         uint64 votingPeriodStartDate,
         uint64 epochEndDate
     ) external override onlyOwner onlyIfNotInitialized {
         ElectionStore storage store = _electionStore();
 
+        uint8 seatCount = uint8(firstCouncil.length);
+        if (minimumActiveMembers == 0 || minimumActiveMembers > seatCount) {
+            revert InvalidMinimumActiveMembers();
+        }
+
         ElectionSettings storage settings = _electionSettings();
         settings.minNominationPeriodDuration = 2 days;
         settings.minVotingPeriodDuration = 2 days;
         settings.minEpochDuration = 7 days;
         settings.maxDateAdjustmentTolerance = 7 days;
-        settings.nextEpochSeatCount = 3;
+        settings.nextEpochSeatCount = uint8(firstCouncil.length);
+        settings.minimumActiveMembers = minimumActiveMembers;
         settings.defaultBallotEvaluationBatchSize = 500;
 
         store.currentEpochIndex = 1;
@@ -46,7 +54,7 @@ contract ElectionModule is
         _configureEpochSchedule(firstEpoch, epochStartDate, nominationPeriodStartDate, votingPeriodStartDate, epochEndDate);
 
         _createCouncilToken(councilTokenName, councilTokenSymbol);
-        _addCouncilMember(msg.sender);
+        _addCouncilMembers(firstCouncil);
 
         store.initialized = true;
 
@@ -138,6 +146,29 @@ contract ElectionModule is
         emit NextEpochSeatCountChanged(newSeatCount);
     }
 
+    function setMinimumActiveMembers(uint8 newMinimumActiveMembers) external onlyOwner {
+        if (newMinimumActiveMembers == 0) revert InvalidMinimumActiveMembers();
+
+        _electionSettings().minimumActiveMembers = newMinimumActiveMembers;
+
+        emit MinimumActiveMembersChanged(newMinimumActiveMembers);
+    }
+
+    /// @notice Allows the owner to remove one or more council members, triggering an election if a threshold is met
+    function dismissMembers(address[] calldata membersToDismiss) external override onlyOwner {
+        _removeCouncilMembers(membersToDismiss);
+
+        emit CouncilMembersDismissed(membersToDismiss);
+
+        // Don't immediately jump to an election if the council still has enough members
+        if (_getCurrentPeriod() != ElectionPeriod.Idle) return;
+        if (_electionStore().councilMembers.length() >= _electionSettings().minimumActiveMembers) return;
+
+        _jumpToNominationPeriod();
+
+        emit EmergencyElectionStarted();
+    }
+
     /// @notice Allows anyone to self-nominate during the Nomination period
     function nominate() external override onlyInPeriod(ElectionPeriod.Nomination) {
         SetUtil.AddressSet storage nominees = _getCurrentElection().nominees;
@@ -162,7 +193,7 @@ contract ElectionModule is
 
     /// @notice Allows anyone with vote power to vote on nominated candidates during the Voting period
     /// @dev ElectionVotes needs to be extended to specify what determines voting power
-    function elect(address[] calldata candidates) external override onlyInPeriod(ElectionPeriod.Vote) {
+    function cast(address[] calldata candidates) external override onlyInPeriod(ElectionPeriod.Vote) {
         uint votePower = _getVotePower(msg.sender);
 
         if (votePower == 0) revert NoVotePower();
@@ -208,7 +239,7 @@ contract ElectionModule is
         if (!isElectionEvaluated()) revert ElectionNotEvaluated();
 
         _removeAllCouncilMembers();
-        _addCouncilMembers(_getCurrentElection().winners);
+        _addCouncilMembers(_getCurrentElection().winners.values());
 
         _getCurrentElection().resolved = true;
 
@@ -253,6 +284,11 @@ contract ElectionModule is
         return _electionSettings().nextEpochSeatCount;
     }
 
+    /// @notice Returns the minimum active members that the council needs to avoid an emergency election
+    function getMinimumActiveMembers() external view returns (uint8) {
+        return _electionSettings().minimumActiveMembers;
+    }
+
     /// @notice Returns the index of the current epoch. The first epoch's index is 1
     function getEpochIndex() external view override returns (uint) {
         return _electionStore().currentEpochIndex;
diff --git a/packages/core-modules/contracts/storage/ElectionStorage.sol b/packages/core-modules/contracts/storage/ElectionStorage.sol
index 218728cf..f5ddba0f 100644
--- a/packages/core-modules/contracts/storage/ElectionStorage.sol
+++ b/packages/core-modules/contracts/storage/ElectionStorage.sol
@@ -28,6 +28,8 @@ contract ElectionStorage {
     struct ElectionSettings {
         // Number of council members in the next epoch
         uint8 nextEpochSeatCount;
+        // Minimum active council members. If too many are dismissed an emergency election is triggered
+        uint8 minimumActiveMembers;
         // Minimum epoch duration when adjusting schedules
         uint64 minEpochDuration;
         // Minimum nomination period duration when adjusting schedules
diff --git a/packages/core-modules/contracts/submodules/election/ElectionBase.sol b/packages/core-modules/contracts/submodules/election/ElectionBase.sol
index 9bae9c33..996dc3e9 100644
--- a/packages/core-modules/contracts/submodules/election/ElectionBase.sol
+++ b/packages/core-modules/contracts/submodules/election/ElectionBase.sol
@@ -38,6 +38,7 @@ contract ElectionBase is ElectionStorage, InitializableMixin {
     error ChangesCurrentPeriod();
     error AlreadyACouncilMember();
     error NotACouncilMember();
+    error InvalidMinimumActiveMembers();
 
     // ---------------------------------------
     // Events
@@ -49,6 +50,7 @@ contract ElectionBase is ElectionStorage, InitializableMixin {
     event CouncilTokenUpgraded(address newImplementation);
     event CouncilMemberAdded(address member);
     event CouncilMemberRemoved(address member);
+    event CouncilMembersDismissed(address[] members);
     event EpochScheduleUpdated(uint64 nominationPeriodStartDate, uint64 votingPeriodStartDate, uint64 epochEndDate);
     event MinimumEpochDurationsChanged(
         uint64 minNominationPeriodDuration,
@@ -58,12 +60,14 @@ contract ElectionBase is ElectionStorage, InitializableMixin {
     event MaxDateAdjustmentToleranceChanged(uint64 tolerance);
     event DefaultBallotEvaluationBatchSizeChanged(uint size);
     event NextEpochSeatCountChanged(uint seatCount);
+    event MinimumActiveMembersChanged(uint8 minimumActiveMembers);
     event CandidateNominated(address indexed candidate);
     event NominationWithdrawn(address indexed candidate);
     event VoteRecorded(address indexed voter, bytes32 indexed ballotId, uint votePower);
     event VoteWithdrawn(address indexed voter, bytes32 indexed ballotId, uint votePower);
     event ElectionEvaluated(uint epochIndex, uint totalBallots);
     event ElectionBatchEvaluated(uint epochIndex, uint evaluatedBallots, uint totalBallots);
+    event EmergencyElectionStarted();
 
     // ---------------------------------------
     // Helpers
diff --git a/packages/core-modules/contracts/submodules/election/ElectionCredentials.sol b/packages/core-modules/contracts/submodules/election/ElectionCredentials.sol
index 0db3381a..c35db1e8 100644
--- a/packages/core-modules/contracts/submodules/election/ElectionCredentials.sol
+++ b/packages/core-modules/contracts/submodules/election/ElectionCredentials.sol
@@ -2,6 +2,7 @@
 pragma solidity ^0.8.0;
 
 import "@synthetixio/core-contracts/contracts/proxy/UUPSProxy.sol";
+import "@synthetixio/core-contracts/contracts/errors/ArrayError.sol";
 import "../../tokens/CouncilToken.sol";
 import "./ElectionBase.sol";
 
@@ -26,14 +27,6 @@ contract ElectionCredentials is ElectionBase {
         emit CouncilTokenCreated(address(proxy), address(implementation));
     }
 
-    function _addCouncilMembers(SetUtil.AddressSet storage newMembers) internal {
-        uint numNewMembers = newMembers.length();
-
-        for (uint newMemberPosition = 1; newMemberPosition <= numNewMembers; newMemberPosition++) {
-            _addCouncilMember(newMembers.valueAt(newMemberPosition));
-        }
-    }
-
     function _removeAllCouncilMembers() internal {
         SetUtil.AddressSet storage members = _electionStore().councilMembers;
 
@@ -46,6 +39,24 @@ contract ElectionCredentials is ElectionBase {
         }
     }
 
+    function _addCouncilMembers(address[] memory membersToAdd) internal {
+        uint numMembers = membersToAdd.length;
+        if (numMembers == 0) revert ArrayError.EmptyArray();
+
+        for (uint memberIndex = 0; memberIndex < numMembers; memberIndex++) {
+            _addCouncilMember(membersToAdd[memberIndex]);
+        }
+    }
+
+    function _removeCouncilMembers(address[] memory membersToRemove) internal {
+        uint numMembers = membersToRemove.length;
+        if (numMembers == 0) revert ArrayError.EmptyArray();
+
+        for (uint memberIndex = 0; memberIndex < numMembers; memberIndex++) {
+            _removeCouncilMember(membersToRemove[memberIndex]);
+        }
+    }
+
     function _addCouncilMember(address newMember) internal {
         ElectionStore storage store = _electionStore();
         SetUtil.AddressSet storage members = store.councilMembers;
diff --git a/packages/core-modules/contracts/submodules/election/ElectionSchedule.sol b/packages/core-modules/contracts/submodules/election/ElectionSchedule.sol
index ce78eb2d..8b34007d 100644
--- a/packages/core-modules/contracts/submodules/election/ElectionSchedule.sol
+++ b/packages/core-modules/contracts/submodules/election/ElectionSchedule.sol
@@ -120,6 +120,28 @@ contract ElectionSchedule is ElectionBase {
         }
     }
 
+    /// @dev Moves schedule forward to immediately jump to the nomination period
+    function _jumpToNominationPeriod() internal {
+        EpochData storage currentEpoch = _getCurrentEpoch();
+
+        uint64 nominationPeriodDuration = _getNominationPeriodDuration(currentEpoch);
+        uint64 votingPeriodDuration = _getVotingPeriodDuration(currentEpoch);
+
+        // Keep the previous durations, but shift everything back
+        // so that nominations start now
+        uint64 newNominationPeriodStartDate = uint64(block.timestamp);
+        uint64 newVotingPeriodStartDate = newNominationPeriodStartDate + nominationPeriodDuration;
+        uint64 newEpochEndDate = newVotingPeriodStartDate + votingPeriodDuration;
+
+        _configureEpochSchedule(
+            currentEpoch,
+            currentEpoch.startDate,
+            newNominationPeriodStartDate,
+            newVotingPeriodStartDate,
+            newEpochEndDate
+        );
+    }
+
     /// @dev Copies the current epoch schedule to the next epoch, maintaining durations
     function _copyCurrentEpochScheduleToNextEpoch() internal {
         EpochData storage currentEpoch = _getCurrentEpoch();
diff --git a/packages/core-modules/contracts/submodules/election/ElectionVotes.sol b/packages/core-modules/contracts/submodules/election/ElectionVotes.sol
index c5108ed0..1451be99 100644
--- a/packages/core-modules/contracts/submodules/election/ElectionVotes.sol
+++ b/packages/core-modules/contracts/submodules/election/ElectionVotes.sol
@@ -4,7 +4,7 @@ pragma solidity ^0.8.0;
 import "./ElectionBase.sol";
 import "@synthetixio/core-contracts/contracts/errors/ChangeError.sol";
 
-/// @dev Defines core functionality for recording votes in ElectionModule.elect()
+/// @dev Defines core functionality for recording votes in ElectionModule.cast()
 abstract contract ElectionVotes is ElectionBase {
     using SetUtil for SetUtil.AddressSet;
 
diff --git a/packages/core-modules/contracts/submodules/election/strategies/ElectionVotesBasic.sol b/packages/core-modules/contracts/submodules/election/strategies/ElectionVotesBasic.sol
index 009751cd..cb227d0e 100644
--- a/packages/core-modules/contracts/submodules/election/strategies/ElectionVotesBasic.sol
+++ b/packages/core-modules/contracts/submodules/election/strategies/ElectionVotesBasic.sol
@@ -3,7 +3,7 @@ pragma solidity ^0.8.0;
 
 import "../ElectionVotes.sol";
 
-/// @dev Basic placeholder implementation of determining vote power in ElectionModule.elect() - do not use in production!
+/// @dev Basic placeholder implementation of determining vote power in ElectionModule.cast() - do not use in production!
 contract ElectionVotesBasic is ElectionVotes {
     function _getVotePower(address) internal view virtual override returns (uint) {
         return 1;
diff --git a/packages/deployer/task-names.js b/packages/deployer/task-names.js
index 68c9fc5c..e419f6bf 100644
--- a/packages/deployer/task-names.js
+++ b/packages/deployer/task-names.js
@@ -7,6 +7,7 @@ module.exports = {
   SUBTASK_DEPLOY_MODULES: 'deployer-generate-deploy-modules',
   SUBTASK_DEPLOY_PROXY: 'deployer-deploy-proxy',
   SUBTASK_DEPLOY_ROUTER: 'deployer-generate-deploy-router',
+  SUBTASK_FINALIZE_DEPLOYMENT: 'deployer-finalize-deployment',
   SUBTASK_GENERATE_ROUTER_SOURCE: 'deployer-generate-router-source',
   SUBTASK_LOAD_DEPLOYMENT: 'deployer-load-deployment',
   SUBTASK_PRINT_INFO: 'deployer-print-info',


Test patch:
diff --git a/packages/core-js/test/utils/ethers/events.test.js b/packages/core-js/test/utils/ethers/events.test.js
index 4738635c..d651a708 100644
--- a/packages/core-js/test/utils/ethers/events.test.js
+++ b/packages/core-js/test/utils/ethers/events.test.js
@@ -15,10 +15,8 @@ describe('utils/ethers/events.js', () => {
     assert.equal(event.args.value, '42');
   });
 
-  it('throws if it cannot find any event', async () => {
-    assert.throws(() => findEvent({ receipt: unparsedTxReceipt, eventName: 'InvalidEvent' }), {
-      message: 'Cannot find event InvalidEvent in receipt.',
-    });
+  it('returns undefined if it cannot find any event', async () => {
+    assert.equal(findEvent({ receipt: unparsedTxReceipt, eventName: 'InvalidEvent' }), undefined);
   });
 
   it('can retrieve events from a deployment transaction receipt if a contract interface is provided', async () => {
diff --git a/packages/core-modules/test/contracts/modules/ElectionModule/Dismiss.test.js b/packages/core-modules/test/contracts/modules/ElectionModule/Dismiss.test.js
new file mode 100644
index 00000000..0ef47df7
--- /dev/null
+++ b/packages/core-modules/test/contracts/modules/ElectionModule/Dismiss.test.js
@@ -0,0 +1,335 @@
+const { ethers } = hre;
+const assert = require('assert/strict');
+const assertBn = require('@synthetixio/core-js/utils/assertions/assert-bignumber');
+const assertRevert = require('@synthetixio/core-js/utils/assertions/assert-revert');
+const { daysToSeconds } = require('@synthetixio/core-js/utils/misc/dates');
+const { bootstrap } = require('@synthetixio/deployer/utils/tests');
+const initializer = require('../../../helpers/initializer');
+const { ElectionPeriod } = require('./helpers/election-helper');
+const { findEvent } = require('@synthetixio/core-js/utils/ethers/events');
+const { runElection } = require('./helpers/election-helper');
+const {
+  getTime,
+  fastForwardTo,
+  takeSnapshot,
+  restoreSnapshot,
+} = require('@synthetixio/core-js/utils/hardhat/rpc');
+
+describe('ElectionModule (dismiss)', () => {
+  const { proxyAddress } = bootstrap(initializer);
+
+  let members;
+
+  let owner, user1, user2, user3, user4;
+
+  let ElectionModule, CouncilToken;
+
+  let snapshotId;
+
+  let nominationPeriodStartDate, votingPeriodStartDate, epochEndDate;
+
+  let receipt;
+
+  async function itHasExpectedMembers() {
+    it('shows that the members are in the council', async function () {
+      assert.deepEqual(
+        await ElectionModule.getCouncilMembers(),
+        members.map((m) => m.address)
+      );
+    });
+
+    it('shows that all members hold their corresponding NFT', async function () {
+      for (let member of members) {
+        assertBn.equal(await CouncilToken.balanceOf(member.address), 1);
+
+        const tokenId = members.indexOf(member) + 1;
+        assert.equal(await CouncilToken.ownerOf(tokenId), member.address);
+      }
+    });
+  }
+
+  before('identify signers', async () => {
+    const users = await ethers.getSigners();
+
+    [owner, user1, user2, user3, user4] = users;
+  });
+
+  before('identify modules', async () => {
+    ElectionModule = await ethers.getContractAt('ElectionModule', proxyAddress());
+  });
+
+  describe('when the module is initialized', function () {
+    before('initialize', async function () {
+      const now = await getTime(ethers.provider);
+      const epochEndDate = now + daysToSeconds(90);
+      const votingPeriodStartDate = epochEndDate - daysToSeconds(7);
+      const nominationPeriodStartDate = votingPeriodStartDate - daysToSeconds(7);
+
+      await ElectionModule.initializeElectionModule(
+        'Spartan Council Token',
+        'SCT',
+        [owner.address],
+        1,
+        nominationPeriodStartDate,
+        votingPeriodStartDate,
+        epochEndDate
+      );
+    });
+
+    before('identify the council token', async function () {
+      const tokenAddress = await ElectionModule.getCouncilToken();
+
+      CouncilToken = await ethers.getContractAt('CouncilToken', tokenAddress);
+    });
+
+    describe('after some election', function () {
+      before('run election', async function () {
+        members = [owner, user1, user2];
+        await runElection(ElectionModule, owner, members);
+      });
+
+      before('configure minimum seat count', async function () {
+        await ElectionModule.connect(owner).setMinimumActiveMembers(2);
+      });
+
+      itHasExpectedMembers();
+    });
+
+    describe('when a non-owner account attempts to dismiss a council member', function () {
+      it('reverts', async function () {
+        await assertRevert(
+          ElectionModule.connect(user1).dismissMembers([owner.address]),
+          'Unauthorized'
+        );
+      });
+    });
+
+    describe('when the owner attempts to dismiss a non-existing member', function () {
+      it('reverts', async function () {
+        await assertRevert(
+          ElectionModule.connect(owner).dismissMembers([user3.address]),
+          'NotACouncilMember'
+        );
+      });
+    });
+
+    describe('when the owner dismisses council members, but does not trigger an emergency election', function () {
+      before('take snapshot', async function () {
+        snapshotId = await takeSnapshot(ethers.provider);
+      });
+
+      after('restore snapshot', async function () {
+        await restoreSnapshot(snapshotId, ethers.provider);
+      });
+
+      before('dismiss', async function () {
+        const tx = await ElectionModule.connect(owner).dismissMembers([user2.address]);
+        receipt = await tx.wait();
+
+        members = [owner, user1];
+      });
+
+      itHasExpectedMembers();
+
+      it('emitted a CouncilMembersDismissed event', async function () {
+        const event = findEvent({ receipt, eventName: 'CouncilMembersDismissed' });
+
+        assert.ok(event);
+        assert.deepEqual(event.args.members, [user2.address]);
+      });
+
+      it('shows that the current period is Idle', async function () {
+        assertBn.equal(await ElectionModule.getCurrentPeriod(), ElectionPeriod.Idle);
+      });
+    });
+  });
+
+  describe('when the owner dismisses council members and triggers and emergency election', function () {
+    describe('while in the Idle period', function () {
+      before('take snapshot', async function () {
+        snapshotId = await takeSnapshot(ethers.provider);
+      });
+
+      after('restore snapshot', async function () {
+        await restoreSnapshot(snapshotId, ethers.provider);
+      });
+
+      before('record schedule', async function () {
+        nominationPeriodStartDate = await ElectionModule.getNominationPeriodStartDate();
+        votingPeriodStartDate = await ElectionModule.getVotingPeriodStartDate();
+        epochEndDate = await ElectionModule.getEpochEndDate();
+      });
+
+      before('dismiss', async function () {
+        const tx = await ElectionModule.connect(owner).dismissMembers([
+          user1.address,
+          user2.address,
+        ]);
+        receipt = await tx.wait();
+
+        members = [owner];
+      });
+
+      itHasExpectedMembers();
+
+      it('emitted a CouncilMembersDismissed event', async function () {
+        const event = findEvent({ receipt, eventName: 'CouncilMembersDismissed' });
+
+        assert.ok(event);
+        assert.deepEqual(event.args.members, [user1.address, user2.address]);
+      });
+
+      it('emitted an EmergencyElectionStarted event', async function () {
+        const event = findEvent({ receipt, eventName: 'EmergencyElectionStarted' });
+
+        assert.ok(event);
+      });
+
+      it('shows that the current period is Nomination', async function () {
+        assertBn.equal(await ElectionModule.getCurrentPeriod(), ElectionPeriod.Nomination);
+      });
+
+      it('shows that the schedule has been moved forward', async function () {
+        assertBn.gt(nominationPeriodStartDate, await ElectionModule.getNominationPeriodStartDate());
+        assertBn.gt(votingPeriodStartDate, await ElectionModule.getVotingPeriodStartDate());
+        assertBn.gt(epochEndDate, await ElectionModule.getEpochEndDate());
+      });
+
+      it('allows candidates to nominate', async function () {
+        await ElectionModule.connect(user2).nominate();
+        await ElectionModule.connect(user3).nominate();
+        await ElectionModule.connect(user4).nominate();
+      });
+    });
+
+    describe('while in the Nomination period', function () {
+      before('take snapshot', async function () {
+        snapshotId = await takeSnapshot(ethers.provider);
+      });
+
+      after('restore snapshot', async function () {
+        await restoreSnapshot(snapshotId, ethers.provider);
+      });
+
+      before('record schedule', async function () {
+        nominationPeriodStartDate = await ElectionModule.getNominationPeriodStartDate();
+        votingPeriodStartDate = await ElectionModule.getVotingPeriodStartDate();
+        epochEndDate = await ElectionModule.getEpochEndDate();
+      });
+
+      before('fast forward to the Nomination period', async function () {
+        await fastForwardTo(await ElectionModule.getNominationPeriodStartDate(), ethers.provider);
+      });
+
+      before('dismiss', async function () {
+        const tx = await ElectionModule.connect(owner).dismissMembers([
+          user1.address,
+          user2.address,
+        ]);
+        receipt = await tx.wait();
+
+        members = [owner];
+      });
+
+      itHasExpectedMembers();
+
+      it('emitted a CouncilMembersDismissed event', async function () {
+        const event = findEvent({ receipt, eventName: 'CouncilMembersDismissed' });
+
+        assert.ok(event);
+        assert.deepEqual(event.args.members, [user1.address, user2.address]);
+      });
+
+      it('did not emitted an EmergencyElectionStarted event', async function () {
+        const event = findEvent({ receipt, eventName: 'EmergencyElectionStarted' });
+
+        assert.equal(event, undefined);
+      });
+
+      it('shows that the schedule has not moved', async function () {
+        assertBn.equal(
+          nominationPeriodStartDate,
+          await ElectionModule.getNominationPeriodStartDate()
+        );
+        assertBn.equal(votingPeriodStartDate, await ElectionModule.getVotingPeriodStartDate());
+        assertBn.equal(epochEndDate, await ElectionModule.getEpochEndDate());
+      });
+
+      it('allows candidates to nominate', async function () {
+        await ElectionModule.connect(user2).nominate();
+        await ElectionModule.connect(user3).nominate();
+        await ElectionModule.connect(user4).nominate();
+      });
+    });
+
+    describe('while in the Voting period', function () {
+      before('take snapshot', async function () {
+        snapshotId = await takeSnapshot(ethers.provider);
+      });
+
+      after('restore snapshot', async function () {
+        await restoreSnapshot(snapshotId, ethers.provider);
+      });
+
+      before('record schedule', async function () {
+        nominationPeriodStartDate = await ElectionModule.getNominationPeriodStartDate();
+        votingPeriodStartDate = await ElectionModule.getVotingPeriodStartDate();
+        epochEndDate = await ElectionModule.getEpochEndDate();
+      });
+
+      before('fast forward to the Nomination period and nominate', async function () {
+        await fastForwardTo(await ElectionModule.getNominationPeriodStartDate(), ethers.provider);
+
+        await ElectionModule.connect(user2).nominate();
+        await ElectionModule.connect(user3).nominate();
+        await ElectionModule.connect(user4).nominate();
+      });
+
+      before('fast forward to the Voting period', async function () {
+        await fastForwardTo(await ElectionModule.getVotingPeriodStartDate(), ethers.provider);
+      });
+
+      before('dismiss', async function () {
+        const tx = await ElectionModule.connect(owner).dismissMembers([
+          user1.address,
+          user2.address,
+        ]);
+        receipt = await tx.wait();
+
+        members = [owner];
+      });
+
+      itHasExpectedMembers();
+
+      it('emitted a CouncilMembersDismissed event', async function () {
+        const event = findEvent({ receipt, eventName: 'CouncilMembersDismissed' });
+
+        assert.ok(event);
+        assert.deepEqual(event.args.members, [user1.address, user2.address]);
+      });
+
+      it('did not emitted an EmergencyElectionStarted event', async function () {
+        const event = findEvent({ receipt, eventName: 'EmergencyElectionStarted' });
+
+        assert.equal(event, undefined);
+      });
+
+      it('shows that the schedule has not moved', async function () {
+        assertBn.equal(
+          nominationPeriodStartDate,
+          await ElectionModule.getNominationPeriodStartDate()
+        );
+        assertBn.equal(votingPeriodStartDate, await ElectionModule.getVotingPeriodStartDate());
+        assertBn.equal(epochEndDate, await ElectionModule.getEpochEndDate());
+      });
+
+      it('allows users to vote', async function () {
+        const candidates = [user2.address, user3.address, user4.address];
+
+        await ElectionModule.connect(user2).cast(candidates);
+        await ElectionModule.connect(user3).cast(candidates);
+        await ElectionModule.connect(user4).cast(candidates);
+      });
+    });
+  });
+});
diff --git a/packages/core-modules/test/contracts/modules/ElectionModule/Elect.test.js b/packages/core-modules/test/contracts/modules/ElectionModule/Elect.test.js
index a84b001a..8ed9e6c3 100644
--- a/packages/core-modules/test/contracts/modules/ElectionModule/Elect.test.js
+++ b/packages/core-modules/test/contracts/modules/ElectionModule/Elect.test.js
@@ -42,6 +42,8 @@ describe('ElectionModule (vote)', () => {
       await ElectionModule.initializeElectionModule(
         'Spartan Council Token',
         'SCT',
+        [candidate1.address],
+        1,
         nominationPeriodStartDate,
         votingPeriodStartDate,
         epochEndDate
@@ -77,14 +79,14 @@ describe('ElectionModule (vote)', () => {
           describe('when issuing invalid votes', function () {
             describe('when voting with zero candidates', function () {
               it('reverts', async function () {
-                await assertRevert(ElectionModule.elect([]), 'NoCandidates');
+                await assertRevert(ElectionModule.cast([]), 'NoCandidates');
               });
             });
 
             describe('when voting candidates that are not nominated', function () {
               it('reverts', async function () {
                 await assertRevert(
-                  ElectionModule.elect([candidate1.address, candidate2.address, voter1.address]),
+                  ElectionModule.cast([candidate1.address, candidate2.address, voter1.address]),
                   'NotNominated'
                 );
               });
@@ -93,11 +95,7 @@ describe('ElectionModule (vote)', () => {
             describe('when voting duplicate candidates', function () {
               it('reverts', async function () {
                 await assertRevert(
-                  ElectionModule.elect([
-                    candidate1.address,
-                    candidate2.address,
-                    candidate1.address,
-                  ]),
+                  ElectionModule.cast([candidate1.address, candidate2.address, candidate1.address]),
                   'DuplicateCandidates'
                 );
               });
@@ -130,14 +128,14 @@ describe('ElectionModule (vote)', () => {
             });
 
             before('vote', async function () {
-              await ElectionModule.connect(voter1).elect(ballot1.candidates);
-              await ElectionModule.connect(voter2).elect(ballot2.candidates);
-              await ElectionModule.connect(voter3).elect(ballot1.candidates);
-              await ElectionModule.connect(voter4).elect(ballot1.candidates);
+              await ElectionModule.connect(voter1).cast(ballot1.candidates);
+              await ElectionModule.connect(voter2).cast(ballot2.candidates);
+              await ElectionModule.connect(voter3).cast(ballot1.candidates);
+              await ElectionModule.connect(voter4).cast(ballot1.candidates);
             });
 
             before('vote and record receipt', async function () {
-              const tx = await ElectionModule.connect(voter5).elect(ballot2.candidates);
+              const tx = await ElectionModule.connect(voter5).cast(ballot2.candidates);
               receipt = await tx.wait();
             });
 
@@ -181,7 +179,7 @@ describe('ElectionModule (vote)', () => {
 
             describe('when users change their vote', function () {
               before('change vote', async function () {
-                const tx = await ElectionModule.connect(voter5).elect(ballot3.candidates);
+                const tx = await ElectionModule.connect(voter5).cast(ballot3.candidates);
                 receipt = await tx.wait();
               });
 
diff --git a/packages/core-modules/test/contracts/modules/ElectionModule/Evaluate.test.js b/packages/core-modules/test/contracts/modules/ElectionModule/Evaluate.test.js
index 439e77ea..9270a32e 100644
--- a/packages/core-modules/test/contracts/modules/ElectionModule/Evaluate.test.js
+++ b/packages/core-modules/test/contracts/modules/ElectionModule/Evaluate.test.js
@@ -45,12 +45,18 @@ describe('ElectionModule (evaluate)', () => {
       await ElectionModule.initializeElectionModule(
         'Spartan Council Token',
         'SCT',
+        [candidate1.address],
+        1,
         nominationPeriodStartDate,
         votingPeriodStartDate,
         epochEndDate
       );
     });
 
+    before('configure next epoch seat count', async function () {
+      await ElectionModule.connect(candidate1).setNextEpochSeatCount(3);
+    });
+
     describe('when entering the nomiantion period', function () {
       before('fast forward', async function () {
         await fastForwardTo(await ElectionModule.getNominationPeriodStartDate(), ethers.provider);
@@ -89,16 +95,16 @@ describe('ElectionModule (evaluate)', () => {
         });
 
         before('vote', async function () {
-          await ElectionModule.connect(voter1).elect(ballot1.candidates);
-          await ElectionModule.connect(voter2).elect(ballot1.candidates);
-          await ElectionModule.connect(voter3).elect(ballot1.candidates);
-          await ElectionModule.connect(voter4).elect(ballot1.candidates);
-          await ElectionModule.connect(voter5).elect(ballot2.candidates);
-          await ElectionModule.connect(voter6).elect(ballot3.candidates);
-          await ElectionModule.connect(voter7).elect(ballot3.candidates);
-          await ElectionModule.connect(voter8).elect(ballot3.candidates);
-          await ElectionModule.connect(voter9).elect(ballot3.candidates);
-          await ElectionModule.connect(voter10).elect(ballot3.candidates);
+          await ElectionModule.connect(voter1).cast(ballot1.candidates);
+          await ElectionModule.connect(voter2).cast(ballot1.candidates);
+          await ElectionModule.connect(voter3).cast(ballot1.candidates);
+          await ElectionModule.connect(voter4).cast(ballot1.candidates);
+          await ElectionModule.connect(voter5).cast(ballot2.candidates);
+          await ElectionModule.connect(voter6).cast(ballot3.candidates);
+          await ElectionModule.connect(voter7).cast(ballot3.candidates);
+          await ElectionModule.connect(voter8).cast(ballot3.candidates);
+          await ElectionModule.connect(voter9).cast(ballot3.candidates);
+          await ElectionModule.connect(voter10).cast(ballot3.candidates);
         });
 
         it('shows that ballots were registered', async function () {
diff --git a/packages/core-modules/test/contracts/modules/ElectionModule/Initialize.test.js b/packages/core-modules/test/contracts/modules/ElectionModule/Initialize.test.js
index cf109e0b..d4b81d14 100644
--- a/packages/core-modules/test/contracts/modules/ElectionModule/Initialize.test.js
+++ b/packages/core-modules/test/contracts/modules/ElectionModule/Initialize.test.js
@@ -12,7 +12,7 @@ const { findEvent } = require('@synthetixio/core-js/utils/ethers/events');
 describe('ElectionModule (initialization)', () => {
   const { proxyAddress } = bootstrap(initializer);
 
-  let ElectionModule;
+  let ElectionModule, CouncilToken;
 
   let owner, user;
 
@@ -51,7 +51,7 @@ describe('ElectionModule (initialization)', () => {
     describe('with an account that does not own the instance', function () {
       it('reverts', async function () {
         await assertRevert(
-          ElectionModule.connect(user).initializeElectionModule('', '', 0, 0, 0),
+          ElectionModule.connect(user).initializeElectionModule('', '', [], 0, 0, 0, 0),
           'Unauthorized'
         );
       });
@@ -59,23 +59,76 @@ describe('ElectionModule (initialization)', () => {
 
     describe('with the account that owns the instance', function () {
       describe('with invalid parameters', function () {
+        describe('with invalid minimumActiveMembers', function () {
+          it('reverts', async function () {
+            await assertRevert(
+              ElectionModule.connect(owner).initializeElectionModule(
+                '',
+                '',
+                [owner.address],
+                2,
+                0,
+                0,
+                0
+              ),
+              'InvalidMinimumActiveMembers'
+            );
+            await assertRevert(
+              ElectionModule.connect(owner).initializeElectionModule(
+                '',
+                '',
+                [owner.address],
+                0,
+                0,
+                0,
+                0
+              ),
+              'InvalidMinimumActiveMembers'
+            );
+          });
+        });
+
         describe('with incorrect date order', function () {
           it('reverts', async function () {
             const now = await getTime(ethers.provider);
-            const date1 = now + daysToSeconds(2);
+            const date1 = now + daysToSeconds(7);
             const date2 = date1 + daysToSeconds(2);
             const date3 = date2 + daysToSeconds(2);
 
             await assertRevert(
-              ElectionModule.connect(owner).initializeElectionModule('', '', date2, date1, date3),
+              ElectionModule.connect(owner).initializeElectionModule(
+                '',
+                '',
+                [owner.address],
+                1,
+                date2,
+                date1,
+                date3
+              ),
               'InvalidEpochConfiguration'
             );
             await assertRevert(
-              ElectionModule.initializeElectionModule('', '', date1, date3, date2),
+              ElectionModule.initializeElectionModule(
+                '',
+                '',
+                [owner.address],
+                1,
+                date1,
+                date3,
+                date2
+              ),
               'InvalidEpochConfiguration'
             );
             await assertRevert(
-              ElectionModule.initializeElectionModule('', '', date3, date2, date1),
+              ElectionModule.initializeElectionModule(
+                '',
+                '',
+                [owner.address],
+                1,
+                date3,
+                date2,
+                date1
+              ),
               'InvalidEpochConfiguration'
             );
           });
@@ -92,6 +145,8 @@ describe('ElectionModule (initialization)', () => {
               ElectionModule.connect(owner).initializeElectionModule(
                 '',
                 '',
+                [owner.address],
+                1,
                 nominationPeriodStartDate,
                 votingPeriodStartDate,
                 epochEndDate
@@ -106,6 +161,8 @@ describe('ElectionModule (initialization)', () => {
               ElectionModule.connect(owner).initializeElectionModule(
                 '',
                 '',
+                [owner.address],
+                1,
                 nominationPeriodStartDate,
                 votingPeriodStartDate,
                 epochEndDate
@@ -120,6 +177,8 @@ describe('ElectionModule (initialization)', () => {
               ElectionModule.connect(owner).initializeElectionModule(
                 '',
                 '',
+                [owner.address],
+                1,
                 now + daysToSeconds(1),
                 now + daysToSeconds(2),
                 now + daysToSeconds(7)
@@ -138,8 +197,10 @@ describe('ElectionModule (initialization)', () => {
           nominationPeriodStartDate = votingPeriodStartDate - daysToSeconds(7);
 
           const tx = await ElectionModule.initializeElectionModule(
-            '',
-            '',
+            'Spartan Council Token',
+            'SCT',
+            [owner.address, user.address],
+            1,
             nominationPeriodStartDate,
             votingPeriodStartDate,
             epochEndDate
@@ -147,6 +208,17 @@ describe('ElectionModule (initialization)', () => {
           receipt = await tx.wait();
         });
 
+        before('identify the council token', async function () {
+          const tokenAddress = await ElectionModule.getCouncilToken();
+
+          CouncilToken = await ethers.getContractAt('CouncilToken', tokenAddress);
+        });
+
+        it('produced a token with the correct name and symbol', async function () {
+          assert.equal(await CouncilToken.name(), 'Spartan Council Token');
+          assert.equal(await CouncilToken.symbol(), 'SCT');
+        });
+
         it('emitted an ElectionModuleInitialized event', async function () {
           const event = findEvent({ receipt, eventName: 'ElectionModuleInitialized' });
 
@@ -172,8 +244,18 @@ describe('ElectionModule (initialization)', () => {
           assertBn.equal(await ElectionModule.getCurrentPeriod(), ElectionPeriod.Idle);
         });
 
-        it('shows that the owner is the single council member', async function () {
-          assert.deepEqual(await ElectionModule.getCouncilMembers(), [owner.address]);
+        it('shows that there are two council members', async function () {
+          assert.deepEqual(await ElectionModule.getCouncilMembers(), [owner.address, user.address]);
+        });
+
+        it('shows that there are two council NFTs', async function () {
+          assertBn.equal(await CouncilToken.balanceOf(owner.address), 1);
+          assertBn.equal(await CouncilToken.balanceOf(user.address), 1);
+        });
+
+        it('reflects the expected default settings', async function () {
+          assertBn.equal(await ElectionModule.getNextEpochSeatCount(), 2);
+          assertBn.equal(await ElectionModule.getMinimumActiveMembers(), 1);
         });
 
         it('shows that the first epoch has appropriate dates', async function () {
@@ -199,7 +281,7 @@ describe('ElectionModule (initialization)', () => {
         describe('when attemting to re-initialize the module', function () {
           it('reverts', async function () {
             await assertRevert(
-              ElectionModule.initializeElectionModule('', '', 0, 0, 0),
+              ElectionModule.initializeElectionModule('', '', [], 0, 0, 0, 0),
               'AlreadyInitialized'
             );
           });
diff --git a/packages/core-modules/test/contracts/modules/ElectionModule/Nominate.test.js b/packages/core-modules/test/contracts/modules/ElectionModule/Nominate.test.js
index 0c25acae..28c08d70 100644
--- a/packages/core-modules/test/contracts/modules/ElectionModule/Nominate.test.js
+++ b/packages/core-modules/test/contracts/modules/ElectionModule/Nominate.test.js
@@ -77,6 +77,8 @@ describe('ElectionModule (nominate)', () => {
       await ElectionModule.initializeElectionModule(
         'Spartan Council Token',
         'SCT',
+        [user.address],
+        1,
         nominationPeriodStartDate,
         votingPeriodStartDate,
         epochEndDate
diff --git a/packages/core-modules/test/contracts/modules/ElectionModule/Resolve.test.js b/packages/core-modules/test/contracts/modules/ElectionModule/Resolve.test.js
index 84711b5e..6ea5baed 100644
--- a/packages/core-modules/test/contracts/modules/ElectionModule/Resolve.test.js
+++ b/packages/core-modules/test/contracts/modules/ElectionModule/Resolve.test.js
@@ -1,11 +1,12 @@
 const { ethers } = hre;
 const assert = require('assert/strict');
 const assertBn = require('@synthetixio/core-js/utils/assertions/assert-bignumber');
-const { getTime, fastForwardTo } = require('@synthetixio/core-js/utils/hardhat/rpc');
+const { getTime } = require('@synthetixio/core-js/utils/hardhat/rpc');
 const { daysToSeconds } = require('@synthetixio/core-js/utils/misc/dates');
 const { bootstrap } = require('@synthetixio/deployer/utils/tests');
 const initializer = require('../../../helpers/initializer');
 const { findEvent } = require('@synthetixio/core-js/utils/ethers/events');
+const { runElection } = require('./helpers/election-helper');
 
 describe('ElectionModule (resolve)', () => {
   const { proxyAddress } = bootstrap(initializer);
@@ -19,34 +20,7 @@ describe('ElectionModule (resolve)', () => {
 
   let receipt;
 
-  async function simulateAndResolveElection() {
-    // Nominate
-    await fastForwardTo(await ElectionModule.getNominationPeriodStartDate(), ethers.provider);
-    for (let member of members) {
-      await ElectionModule.connect(member).nominate();
-    }
-
-    // Vote
-    await fastForwardTo(await ElectionModule.getVotingPeriodStartDate(), ethers.provider);
-    for (let member of members) {
-      await ElectionModule.connect(member).elect([member.address]);
-    }
-
-    // Evaluate
-    await fastForwardTo(await ElectionModule.getEpochEndDate(), ethers.provider);
-    await ElectionModule.evaluate(0);
-    assert.equal(await ElectionModule.isElectionEvaluated(), true);
-    assert.deepEqual(
-      await ElectionModule.getElectionWinners(),
-      members.map((w) => w.address)
-    );
-
-    // Resolve
-    const tx = await ElectionModule.resolve();
-    receipt = await tx.wait();
-  }
-
-  async function itProperlyManagesCredentials() {
+  async function itHasExpectedMembers() {
     it('shows that the members are in the council', async function () {
       assert.deepEqual(
         await ElectionModule.getCouncilMembers(),
@@ -84,6 +58,8 @@ describe('ElectionModule (resolve)', () => {
       await ElectionModule.initializeElectionModule(
         'Spartan Council Token',
         'SCT',
+        [owner.address],
+        1,
         nominationPeriodStartDate,
         votingPeriodStartDate,
         epochEndDate
@@ -105,7 +81,7 @@ describe('ElectionModule (resolve)', () => {
         assertBn.equal(await ElectionModule.getEpochIndex(), 1);
       });
 
-      itProperlyManagesCredentials();
+      itHasExpectedMembers();
 
       describe('epoch 2', function () {
         describe('when members 1, 2, and 3 are elected', function () {
@@ -114,14 +90,14 @@ describe('ElectionModule (resolve)', () => {
           });
 
           before('simulate election', async function () {
-            await simulateAndResolveElection();
+            receipt = await runElection(ElectionModule, owner, members);
           });
 
           it('shows that the current epoch is 2', async function () {
             assertBn.equal(await ElectionModule.getEpochIndex(), 2);
           });
 
-          itProperlyManagesCredentials();
+          itHasExpectedMembers();
 
           it('emitted an EpochStarted event', async function () {
             const event = findEvent({ receipt, eventName: 'EpochStarted' });
@@ -157,14 +133,14 @@ describe('ElectionModule (resolve)', () => {
             });
 
             before('simulate election', async function () {
-              await simulateAndResolveElection();
+              receipt = await runElection(ElectionModule, owner, members);
             });
 
             it('shows that the current epoch is 3', async function () {
               assertBn.equal(await ElectionModule.getEpochIndex(), 3);
             });
 
-            itProperlyManagesCredentials();
+            itHasExpectedMembers();
 
             it('emitted an EpochStarted event', async function () {
               const event = findEvent({ receipt, eventName: 'EpochStarted' });
@@ -204,14 +180,14 @@ describe('ElectionModule (resolve)', () => {
                 });
 
                 before('simulate election', async function () {
-                  await simulateAndResolveElection();
+                  receipt = await runElection(ElectionModule, owner, members);
                 });
 
                 it('shows that the current epoch is 4', async function () {
                   assertBn.equal(await ElectionModule.getEpochIndex(), 4);
                 });
 
-                itProperlyManagesCredentials();
+                itHasExpectedMembers();
 
                 it('emitted an EpochStarted event', async function () {
                   const event = findEvent({ receipt, eventName: 'EpochStarted' });
diff --git a/packages/core-modules/test/contracts/modules/ElectionModule/Schedule.test.js b/packages/core-modules/test/contracts/modules/ElectionModule/Schedule.test.js
index 638c3bed..dbec4972 100644
--- a/packages/core-modules/test/contracts/modules/ElectionModule/Schedule.test.js
+++ b/packages/core-modules/test/contracts/modules/ElectionModule/Schedule.test.js
@@ -74,7 +74,7 @@ describe('ElectionModule (schedule)', () => {
   const itRejectsVotes = () => {
     describe('when trying to call the elect function', function () {
       it('reverts', async function () {
-        await assertRevert(ElectionModule.elect([user.address]), 'NotCallableInCurrentPeriod');
+        await assertRevert(ElectionModule.cast([user.address]), 'NotCallableInCurrentPeriod');
       });
     });
   };
@@ -82,7 +82,7 @@ describe('ElectionModule (schedule)', () => {
   const itAcceptsVotes = () => {
     describe('when trying to call the elect function', function () {
       it('does not revert', async function () {
-        await ElectionModule.elect([user.address]);
+        await ElectionModule.cast([user.address]);
       });
     });
   };
@@ -345,6 +345,8 @@ describe('ElectionModule (schedule)', () => {
       await ElectionModule.initializeElectionModule(
         'Spartan Council Token',
         'SCT',
+        [owner.address],
+        1,
         nominationPeriodStartDate,
         votingPeriodStartDate,
         epochEndDate
diff --git a/packages/core-modules/test/contracts/modules/ElectionModule/Settings.test.js b/packages/core-modules/test/contracts/modules/ElectionModule/Settings.test.js
index f125eb48..4e537398 100644
--- a/packages/core-modules/test/contracts/modules/ElectionModule/Settings.test.js
+++ b/packages/core-modules/test/contracts/modules/ElectionModule/Settings.test.js
@@ -18,14 +18,14 @@ describe('ElectionModule (settings)', () => {
 
   let ElectionModule;
 
-  let user;
+  let owner, user;
 
   let receipt;
 
   let snapshotId;
 
   before('identify signers', async () => {
-    [, user] = await ethers.getSigners();
+    [owner, user] = await ethers.getSigners();
   });
 
   before('identify modules', async () => {
@@ -42,12 +42,66 @@ describe('ElectionModule (settings)', () => {
       await ElectionModule.initializeElectionModule(
         'Spartan Council Token',
         'SCT',
+        [owner.address],
+        1,
         nominationPeriodStartDate,
         votingPeriodStartDate,
         epochEndDate
       );
     });
 
+    // ---------------------------------------
+    // minimumActiveMembers
+    // ---------------------------------------
+
+    describe('when configuring the minimum active members', function () {
+      describe('with an account that is not the owner', () => {
+        it('reverts', async () => {
+          await assertRevert(
+            ElectionModule.connect(user).setMinimumActiveMembers(1),
+            'Unauthorized'
+          );
+        });
+      });
+
+      describe('with the owner', () => {
+        describe('with zero', () => {
+          it('reverts', async () => {
+            await assertRevert(
+              ElectionModule.setMinimumActiveMembers(0),
+              'InvalidMinimumActiveMembers'
+            );
+          });
+        });
+
+        describe('with a value that is too big', () => {
+          it('reverts', async () => {
+            await assertRevert(ElectionModule.setMinimumActiveMembers(1000), 'value out-of-bounds');
+          });
+        });
+
+        describe('with valid parameters', () => {
+          let newMinimumActiveMembers = 3;
+
+          before('set', async () => {
+            const tx = await ElectionModule.setMinimumActiveMembers(newMinimumActiveMembers);
+            receipt = await tx.wait();
+          });
+
+          it('emitted an MinimumActiveMembersChanged event', async function () {
+            const event = findEvent({ receipt, eventName: 'MinimumActiveMembersChanged' });
+
+            assert.ok(event);
+            assertBn.equal(event.args.minimumActiveMembers, newMinimumActiveMembers);
+          });
+
+          it('changes the setting', async () => {
+            assertBn.equal(await ElectionModule.getMinimumActiveMembers(), newMinimumActiveMembers);
+          });
+        });
+      });
+    });
+
     // ---------------------------------------
     // nextEpochSeatCount
     // ---------------------------------------
diff --git a/packages/core-modules/test/contracts/modules/ElectionModule/Token.test.js b/packages/core-modules/test/contracts/modules/ElectionModule/Token.test.js
index 73bb26d9..c2859fd6 100644
--- a/packages/core-modules/test/contracts/modules/ElectionModule/Token.test.js
+++ b/packages/core-modules/test/contracts/modules/ElectionModule/Token.test.js
@@ -46,6 +46,8 @@ describe('ElectionModule (token)', () => {
       const tx = await ElectionModule.initializeElectionModule(
         TOKEN_NAME,
         TOKEN_SYMBOL,
+        [owner.address],
+        1,
         nominationPeriodStartDate,
         votingPeriodStartDate,
         epochEndDate
diff --git a/packages/core-modules/test/contracts/modules/ElectionModule/helpers/election-helper.js b/packages/core-modules/test/contracts/modules/ElectionModule/helpers/election-helper.js
index 021cc539..f455467b 100644
--- a/packages/core-modules/test/contracts/modules/ElectionModule/helpers/election-helper.js
+++ b/packages/core-modules/test/contracts/modules/ElectionModule/helpers/election-helper.js
@@ -1,4 +1,5 @@
-const { ethers } = require('ethers');
+const assert = require('assert/strict');
+const { fastForwardTo } = require('@synthetixio/core-js/utils/hardhat/rpc');
 
 const ElectionPeriod = {
   Idle: 0,
@@ -8,13 +9,46 @@ const ElectionPeriod = {
 };
 
 const assertDatesAreClose = (dateA, dateB) => {
-  const numberDateA = ethers.BigNumber.isBigNumber(dateA) ? dateA.toNumber() : dateA;
-  const numberDateB = ethers.BigNumber.isBigNumber(dateB) ? dateB.toNumber() : dateB;
+  const numberDateA = hre.ethers.BigNumber.isBigNumber(dateA) ? dateA.toNumber() : dateA;
+  const numberDateB = hre.ethers.BigNumber.isBigNumber(dateB) ? dateB.toNumber() : dateB;
 
   return Math.abs(numberDateB - numberDateA) <= 1;
 };
 
+async function runElection(ElectionModule, owner, members) {
+  // Configure
+  await ElectionModule.connect(owner).setNextEpochSeatCount(members.length);
+
+  // Nominate
+  await fastForwardTo(await ElectionModule.getNominationPeriodStartDate(), hre.ethers.provider);
+  for (let member of members) {
+    await ElectionModule.connect(member).nominate();
+  }
+
+  // Vote
+  await fastForwardTo(await ElectionModule.getVotingPeriodStartDate(), hre.ethers.provider);
+  for (let member of members) {
+    await ElectionModule.connect(member).cast([member.address]);
+  }
+
+  // Evaluate
+  await fastForwardTo(await ElectionModule.getEpochEndDate(), hre.ethers.provider);
+  await ElectionModule.evaluate(0);
+  assert.equal(await ElectionModule.isElectionEvaluated(), true);
+  assert.deepEqual(
+    await ElectionModule.getElectionWinners(),
+    members.map((w) => w.address)
+  );
+
+  // Resolve
+  const tx = await ElectionModule.resolve();
+  const receipt = await tx.wait();
+
+  return receipt;
+}
+
 module.exports = {
   ElectionPeriod,
   assertDatesAreClose,
+  runElection,
 };