From 83c4d42f641e1bec77af8ae4cda2048321186376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mati=CC=81as?= Date: Wed, 23 Aug 2023 14:35:06 +0200 Subject: [PATCH 1/4] refactor crosschain helper fns --- .../contracts/interfaces/IElectionModule.sol | 1 - .../modules/core/BaseElectionModule.sol | 50 +++++++++++-------- .../governance/contracts/storage/Council.sol | 2 +- .../governance/contracts/storage/Election.sol | 1 + .../contracts/storage/CrossChain.sol | 19 +++++-- 5 files changed, 47 insertions(+), 26 deletions(-) diff --git a/protocol/governance/contracts/interfaces/IElectionModule.sol b/protocol/governance/contracts/interfaces/IElectionModule.sol index 9f63d37801..63f7cad24d 100644 --- a/protocol/governance/contracts/interfaces/IElectionModule.sol +++ b/protocol/governance/contracts/interfaces/IElectionModule.sol @@ -6,7 +6,6 @@ import {Epoch} from "../storage/Epoch.sol"; /// @title Module for electing a council, represented by a set of NFT holders interface IElectionModule { - error NotMothership(); error AlreadyNominated(); error ElectionAlreadyEvaluated(); error ElectionNotEvaluated(); diff --git a/protocol/governance/contracts/modules/core/BaseElectionModule.sol b/protocol/governance/contracts/modules/core/BaseElectionModule.sol index 56285a17a2..3dfa01f76d 100644 --- a/protocol/governance/contracts/modules/core/BaseElectionModule.sol +++ b/protocol/governance/contracts/modules/core/BaseElectionModule.sol @@ -31,16 +31,6 @@ contract BaseElectionModule is uint256 private constant _CROSSCHAIN_GAS_LIMIT = 100000; - /// @dev Used to allow certain functions to only be executed on the "mothership" chain. - /// The mothership is considered to be the first chain id in the supported networks list. - modifier onlyMothership() { - if (CrossChain.load().getSupportedNetworks()[0] != block.chainid.to64()) { - revert NotMothership(); - } - - _; - } - function initOrUpdateElectionSettings( address[] memory initialCouncil, uint8 minimumActiveMembers, @@ -49,6 +39,7 @@ contract BaseElectionModule is uint64 nominationPeriodDuration, uint64 votingPeriodDuration ) external override { + // TODO: initialization should be called only on mothership and broadcasted? OwnableStorage.onlyOwner(); _initOrUpdateElectionSettings( @@ -153,6 +144,8 @@ contract BaseElectionModule is uint64 newVotingPeriodStartDate, uint64 newEpochEndDate ) external override { + // TODO: onlyOnMothership? + OwnableStorage.onlyOwner(); Council.onlyInPeriod(Council.ElectionPeriod.Administration); Council.Data storage council = Council.load(); @@ -180,6 +173,8 @@ contract BaseElectionModule is uint64 votingPeriodDuration, uint64 maxDateAdjustmentTolerance ) external override { + // TODO: onlyOnMothership? + OwnableStorage.onlyOwner(); Council.onlyInPeriod(Council.ElectionPeriod.Administration); @@ -194,6 +189,8 @@ contract BaseElectionModule is } function dismissMembers(address[] calldata membersToDismiss) external override { + // TODO: onlyOnMothership? + OwnableStorage.onlyOwner(); Council.Data storage store = Council.load(); @@ -219,9 +216,12 @@ contract BaseElectionModule is } function nominate() public virtual override { - SetUtil.AddressSet storage nominees = Council.load().getCurrentElection().nominees; + // TODO: onlyOnMothership? + Council.onlyInPeriod(Council.ElectionPeriod.Nomination); + SetUtil.AddressSet storage nominees = Council.load().getCurrentElection().nominees; + if (nominees.contains(msg.sender)) revert AlreadyNominated(); nominees.add(msg.sender); @@ -232,6 +232,8 @@ contract BaseElectionModule is } function withdrawNomination() external override { + // TODO: onlyOnMothership? + SetUtil.AddressSet storage nominees = Council.load().getCurrentElection().nominees; Council.onlyInPeriod(Council.ElectionPeriod.Nomination); @@ -276,17 +278,15 @@ contract BaseElectionModule is CrossChain.Data storage cc = CrossChain.load(); cc.transmit( - cc.getSupportedNetworks()[0], + cc.getChainIdAt(0), abi.encodeWithSelector(this._recvCast.selector, msg.sender, block.chainid, ballot), _CROSSCHAIN_GAS_LIMIT ); } - function _recvCast( - address voter, - uint256 precinct, - Ballot.Data calldata ballot - ) external onlyMothership { + function _recvCast(address voter, uint256 precinct, Ballot.Data calldata ballot) external { + CrossChain.onlyOnChainAt(0); + CrossChain.onlyCrossChain(); Council.onlyInPeriod(Council.ElectionPeriod.Vote); _validateCandidates(ballot.votedCandidates); @@ -307,11 +307,12 @@ contract BaseElectionModule is election.ballotPtrs.push(ballotPtr); - emit VoteRecorded(msg.sender, precinct, currentElectionId, ballot.votingPower); + emit VoteRecorded(voter, precinct, currentElectionId, ballot.votingPower); } /// @dev ElectionTally needs to be extended to specify how votes are counted - function evaluate(uint numBallots) external override onlyMothership { + function evaluate(uint numBallots) external override { + CrossChain.onlyOnChainAt(0); Council.onlyInPeriod(Council.ElectionPeriod.Evaluation); Election.Data storage election = Council.load().getCurrentElection(); @@ -336,7 +337,8 @@ contract BaseElectionModule is } /// @dev Burns previous NFTs and mints new ones - function resolve() public virtual override onlyMothership { + function resolve() public virtual override { + CrossChain.onlyOnChainAt(0); Council.onlyInPeriod(Council.ElectionPeriod.Evaluation); Council.Data storage store = Council.load(); @@ -358,6 +360,14 @@ contract BaseElectionModule is // TODO: Broadcast message to distribute the new NFTs on all chains } + function _recvResolve(address voter, uint256 precinct, Ballot.Data calldata ballot) external { + CrossChain.onlyOnChainAt(0); + CrossChain.onlyCrossChain(); + Council.onlyInPeriod(Council.ElectionPeriod.Vote); + + // TODO: update voting store, distribute nfts + } + function getEpochSchedule() external view override returns (Epoch.Data memory epoch) { return Council.load().getCurrentElection().epoch; } diff --git a/protocol/governance/contracts/storage/Council.sol b/protocol/governance/contracts/storage/Council.sol index 74542f9e31..8e9370fcaa 100644 --- a/protocol/governance/contracts/storage/Council.sol +++ b/protocol/governance/contracts/storage/Council.sol @@ -27,7 +27,7 @@ library Council { // Council token id's by council member address mapping(address => uint) councilTokenIds; // id of the current epoch - uint currentElectionId; + uint256 currentElectionId; } enum ElectionPeriod { diff --git a/protocol/governance/contracts/storage/Election.sol b/protocol/governance/contracts/storage/Election.sol index 18e22ae7da..1c1b8de6bb 100644 --- a/protocol/governance/contracts/storage/Election.sol +++ b/protocol/governance/contracts/storage/Election.sol @@ -19,6 +19,7 @@ library Election { SetUtil.AddressSet winners; // List of all ballot ids in this election bytes32[] ballotPtrs; + // Total votes count for a given candidate mapping(address => uint256) candidateVoteTotals; } diff --git a/utils/core-modules/contracts/storage/CrossChain.sol b/utils/core-modules/contracts/storage/CrossChain.sol index 6c02ccb9ce..3c04afd412 100644 --- a/utils/core-modules/contracts/storage/CrossChain.sol +++ b/utils/core-modules/contracts/storage/CrossChain.sol @@ -19,6 +19,7 @@ library CrossChain { error NotCcipRouter(address); error UnsupportedNetwork(uint64); + error InvalidNetwork(uint64); error InsufficientCcipFee(uint256 requiredAmount, uint256 availableAmount); error InvalidMessage(); @@ -91,12 +92,22 @@ library CrossChain { } } + function onlyOnChainAt(uint64 chainIndex) internal view { + if (getChainIdAt(load(), chainIndex) != block.chainid.to64()) { + revert InvalidNetwork(block.chainid.to64()); + } + } + + function getChainIdAt(Data storage self, uint64 index) internal view returns (uint64) { + return self.supportedNetworks.valueAt(index + 1).to64(); + } + function getSupportedNetworks(Data storage self) internal view returns (uint64[] memory) { SetUtil.UintSet storage supportedNetworks = self.supportedNetworks; - uint256 supportedNetworksLength = supportedNetworks.length(); - uint64[] memory chains = new uint64[](supportedNetworksLength); - for (uint i = 0; i < supportedNetworksLength; i++) { - uint64 chainId = supportedNetworks.values()[i].to64(); + uint256[] memory supportedChains = supportedNetworks.values(); + uint64[] memory chains = new uint64[](supportedChains.length); + for (uint i = 0; i < supportedChains.length; i++) { + uint64 chainId = supportedChains[i].to64(); chains[i] = chainId; } return chains; From e1a0118f753d9751204a87a76dd3171426c1b925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mati=CC=81as?= Date: Thu, 24 Aug 2023 16:27:09 +0200 Subject: [PATCH 2/4] add nominate during nomination period also --- .../contracts/modules/core/BaseElectionModule.sol | 2 +- protocol/governance/contracts/storage/Council.sol | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/protocol/governance/contracts/modules/core/BaseElectionModule.sol b/protocol/governance/contracts/modules/core/BaseElectionModule.sol index 3dfa01f76d..46623a5a57 100644 --- a/protocol/governance/contracts/modules/core/BaseElectionModule.sol +++ b/protocol/governance/contracts/modules/core/BaseElectionModule.sol @@ -218,7 +218,7 @@ contract BaseElectionModule is function nominate() public virtual override { // TODO: onlyOnMothership? - Council.onlyInPeriod(Council.ElectionPeriod.Nomination); + Council.onlyInPeriods(Council.ElectionPeriod.Nomination, Council.ElectionPeriod.Vote); SetUtil.AddressSet storage nominees = Council.load().getCurrentElection().nominees; diff --git a/protocol/governance/contracts/storage/Council.sol b/protocol/governance/contracts/storage/Council.sol index 8e9370fcaa..81e2fec449 100644 --- a/protocol/governance/contracts/storage/Council.sol +++ b/protocol/governance/contracts/storage/Council.sol @@ -115,6 +115,17 @@ library Council { } } + /// @dev Used to allow certain functions to only operate within a given periods + function onlyInPeriods( + Council.ElectionPeriod period1, + Council.ElectionPeriod period2 + ) internal view { + Council.ElectionPeriod currentPeriod = Council.getCurrentPeriod(load()); + if (currentPeriod != period1 && currentPeriod != period2) { + revert NotCallableInCurrentPeriod(); + } + } + /// @dev Ensures epoch dates are in the correct order, durations are above minimums, etc function validateEpochSchedule( Data storage self, From 615fcfa6b0ec6b11ef505ba9313277663bf8a637 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mati=CC=81as?= Date: Fri, 25 Aug 2023 12:05:37 +0200 Subject: [PATCH 3/4] rename precint to chainId --- .../interfaces/IElectionInspectorModule.sol | 2 +- .../contracts/interfaces/IElectionModule.sol | 8 +++---- .../modules/core/BaseElectionModule.sol | 20 ++++++++-------- .../modules/core/ElectionInspectorModule.sol | 24 ++++++++++--------- .../governance/contracts/storage/Ballot.sol | 8 +++---- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/protocol/governance/contracts/interfaces/IElectionInspectorModule.sol b/protocol/governance/contracts/interfaces/IElectionInspectorModule.sol index 875bdaba48..e41cc315fc 100644 --- a/protocol/governance/contracts/interfaces/IElectionInspectorModule.sol +++ b/protocol/governance/contracts/interfaces/IElectionInspectorModule.sol @@ -29,7 +29,7 @@ interface IElectionInspectorModule { /// @notice Returns if user has voted in the given election function hasVotedInEpoch( address user, - uint precinct, + uint chainId, uint epochIndex ) external view returns (bool); diff --git a/protocol/governance/contracts/interfaces/IElectionModule.sol b/protocol/governance/contracts/interfaces/IElectionModule.sol index 63f7cad24d..5a3d0d5fd0 100644 --- a/protocol/governance/contracts/interfaces/IElectionModule.sol +++ b/protocol/governance/contracts/interfaces/IElectionModule.sol @@ -23,7 +23,7 @@ interface IElectionModule { event NominationWithdrawn(address indexed candidate, uint256 indexed epochId); event VoteRecorded( address indexed voter, - uint256 indexed precinct, + uint256 indexed chainId, uint256 indexed epochId, uint256 votingPower ); @@ -123,19 +123,19 @@ interface IElectionModule { function getNominees() external view returns (address[] memory); /// @notice Returns if user has voted in the current election - function hasVoted(address user, uint256 precinct) external view returns (bool); + function hasVoted(address user, uint256 chainId) external view returns (bool); /// @notice Returns the vote power of user in the current election function getVotePower( address user, - uint256 precinct, + uint256 chainId, uint256 electionId ) external view returns (uint); /// @notice Returns the list of candidates that a particular ballot has function getBallotCandidates( address voter, - uint256 precinct, + uint256 chainId, uint256 electionId ) external view returns (address[] memory); diff --git a/protocol/governance/contracts/modules/core/BaseElectionModule.sol b/protocol/governance/contracts/modules/core/BaseElectionModule.sol index 46623a5a57..bd856a1b34 100644 --- a/protocol/governance/contracts/modules/core/BaseElectionModule.sol +++ b/protocol/governance/contracts/modules/core/BaseElectionModule.sol @@ -284,7 +284,7 @@ contract BaseElectionModule is ); } - function _recvCast(address voter, uint256 precinct, Ballot.Data calldata ballot) external { + function _recvCast(address voter, uint256 chainId, Ballot.Data calldata ballot) external { CrossChain.onlyOnChainAt(0); CrossChain.onlyCrossChain(); Council.onlyInPeriod(Council.ElectionPeriod.Vote); @@ -295,7 +295,7 @@ contract BaseElectionModule is Election.Data storage election = council.getCurrentElection(); uint256 currentElectionId = council.currentElectionId; - Ballot.Data storage storedBallot = Ballot.load(currentElectionId, voter, precinct); + Ballot.Data storage storedBallot = Ballot.load(currentElectionId, voter, chainId); storedBallot.copy(ballot); storedBallot.validate(); @@ -307,7 +307,7 @@ contract BaseElectionModule is election.ballotPtrs.push(ballotPtr); - emit VoteRecorded(voter, precinct, currentElectionId, ballot.votingPower); + emit VoteRecorded(voter, chainId, currentElectionId, ballot.votingPower); } /// @dev ElectionTally needs to be extended to specify how votes are counted @@ -360,7 +360,7 @@ contract BaseElectionModule is // TODO: Broadcast message to distribute the new NFTs on all chains } - function _recvResolve(address voter, uint256 precinct, Ballot.Data calldata ballot) external { + function _recvResolve(address voter, uint256 chainId, Ballot.Data calldata ballot) external { CrossChain.onlyOnChainAt(0); CrossChain.onlyCrossChain(); Council.onlyInPeriod(Council.ElectionPeriod.Vote); @@ -407,27 +407,27 @@ contract BaseElectionModule is return Council.load().getCurrentElection().nominees.values(); } - function hasVoted(address user, uint256 precinct) public view override returns (bool) { + function hasVoted(address user, uint256 chainId) public view override returns (bool) { Council.Data storage council = Council.load(); - Ballot.Data storage ballot = Ballot.load(council.currentElectionId, user, precinct); + Ballot.Data storage ballot = Ballot.load(council.currentElectionId, user, chainId); return ballot.votingPower > 0 && ballot.votedCandidates.length > 0; } function getVotePower( address user, - uint256 precinct, + uint256 chainId, uint256 electionId ) external view override returns (uint) { - Ballot.Data storage ballot = Ballot.load(electionId, user, precinct); + Ballot.Data storage ballot = Ballot.load(electionId, user, chainId); return ballot.votingPower; } function getBallotCandidates( address voter, - uint256 precinct, + uint256 chainId, uint256 electionId ) external view override returns (address[] memory) { - return Ballot.load(electionId, voter, precinct).votedCandidates; + return Ballot.load(electionId, voter, chainId).votedCandidates; } function isElectionEvaluated() public view override returns (bool) { diff --git a/protocol/governance/contracts/modules/core/ElectionInspectorModule.sol b/protocol/governance/contracts/modules/core/ElectionInspectorModule.sol index b8bd492ad9..e7bd1b0ffa 100644 --- a/protocol/governance/contracts/modules/core/ElectionInspectorModule.sol +++ b/protocol/governance/contracts/modules/core/ElectionInspectorModule.sol @@ -10,54 +10,56 @@ contract ElectionInspectorModule is IElectionInspectorModule { using SetUtil for SetUtil.AddressSet; using Ballot for Ballot.Data; - function getEpochStartDateForIndex(uint epochIndex) external view override returns (uint64) { + function getEpochStartDateForIndex(uint256 epochIndex) external view override returns (uint64) { return Election.load(epochIndex).epoch.startDate; } - function getEpochEndDateForIndex(uint epochIndex) external view override returns (uint64) { + function getEpochEndDateForIndex(uint256 epochIndex) external view override returns (uint64) { return Election.load(epochIndex).epoch.endDate; } function getNominationPeriodStartDateForIndex( - uint epochIndex + uint256 epochIndex ) external view override returns (uint64) { return Election.load(epochIndex).epoch.nominationPeriodStartDate; } function getVotingPeriodStartDateForIndex( - uint epochIndex + uint256 epochIndex ) external view override returns (uint64) { return Election.load(epochIndex).epoch.votingPeriodStartDate; } function wasNominated( address candidate, - uint epochIndex + uint256 epochIndex ) external view override returns (bool) { return Election.load(epochIndex).nominees.contains(candidate); } - function getNomineesAtEpoch(uint epochIndex) external view override returns (address[] memory) { + function getNomineesAtEpoch( + uint256 epochIndex + ) external view override returns (address[] memory) { return Election.load(epochIndex).nominees.values(); } function hasVotedInEpoch( address user, - uint precinct, - uint epochIndex + uint256 chainId, + uint256 epochIndex ) external view override returns (bool) { - return Ballot.load(epochIndex, user, precinct).hasVoted(); + return Ballot.load(epochIndex, user, chainId).hasVoted(); } function getCandidateVotesInEpoch( address candidate, - uint epochIndex + uint256 epochIndex ) external view override returns (uint) { return Election.load(epochIndex).candidateVoteTotals[candidate]; } function getElectionWinnersInEpoch( - uint epochIndex + uint256 epochIndex ) external view override returns (address[] memory) { return Election.load(epochIndex).winners.values(); } diff --git a/protocol/governance/contracts/storage/Ballot.sol b/protocol/governance/contracts/storage/Ballot.sol index 96e43ae1ce..e3da34bfac 100644 --- a/protocol/governance/contracts/storage/Ballot.sol +++ b/protocol/governance/contracts/storage/Ballot.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.0; /** * @title Ballot - * @dev A single vote cast by a address/precinct combination. + * @dev A single vote cast by a address/chainId combination. * * A ballot goes through a few stages: * 1. The ballot is empty and all values are 0 @@ -20,12 +20,12 @@ library Ballot { } function load( - uint electionId, + uint256 electionId, address voter, - uint256 precinct + uint256 chainId ) internal pure returns (Data storage self) { bytes32 s = keccak256( - abi.encode("io.synthetix.governance.Ballot", electionId, voter, precinct) + abi.encode("io.synthetix.governance.Ballot", electionId, voter, chainId) ); assembly { From 26e7e51439b9c7adca47801986bff2b016e57090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mati=CC=81as?= Date: Wed, 30 Aug 2023 16:34:04 +0200 Subject: [PATCH 4/4] add nft distribution on satellites --- .../modules/core/BaseElectionModule.sol | 34 +++++++++++-------- protocol/governance/storage.dump.sol | 6 ++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/protocol/governance/contracts/modules/core/BaseElectionModule.sol b/protocol/governance/contracts/modules/core/BaseElectionModule.sol index bd856a1b34..8ac452b900 100644 --- a/protocol/governance/contracts/modules/core/BaseElectionModule.sol +++ b/protocol/governance/contracts/modules/core/BaseElectionModule.sol @@ -227,8 +227,6 @@ contract BaseElectionModule is nominees.add(msg.sender); emit CandidateNominated(msg.sender, Council.load().currentElectionId); - - // TODO: add ballot id to emitted event } function withdrawNomination() external override { @@ -348,24 +346,32 @@ contract BaseElectionModule is uint newEpochIndex = store.currentElectionId + 1; + CrossChain.Data storage cc = CrossChain.load(); + cc.broadcast( + cc.getSupportedNetworks(), + abi.encodeWithSelector( + this._recvResolve.selector, + election.winners.values(), + newEpochIndex + ), + _CROSSCHAIN_GAS_LIMIT + ); + } + + function _recvResolve(address[] calldata winners, uint256 newEpochIndex) external { + CrossChain.onlyOnChainAt(0); + CrossChain.onlyCrossChain(); + + Council.Data storage store = Council.load(); + Election.Data storage election = store.getCurrentElection(); + _removeAllCouncilMembers(newEpochIndex); - _addCouncilMembers(election.winners.values(), newEpochIndex); + _addCouncilMembers(winners, newEpochIndex); election.resolved = true; - store.newElection(); emit EpochStarted(newEpochIndex); - - // TODO: Broadcast message to distribute the new NFTs on all chains - } - - function _recvResolve(address voter, uint256 chainId, Ballot.Data calldata ballot) external { - CrossChain.onlyOnChainAt(0); - CrossChain.onlyCrossChain(); - Council.onlyInPeriod(Council.ElectionPeriod.Vote); - - // TODO: update voting store, distribute nfts } function getEpochSchedule() external view override returns (Epoch.Data memory epoch) { diff --git a/protocol/governance/storage.dump.sol b/protocol/governance/storage.dump.sol index e7d9c06b21..839e078da4 100644 --- a/protocol/governance/storage.dump.sol +++ b/protocol/governance/storage.dump.sol @@ -182,8 +182,8 @@ library Ballot { address[] votedCandidates; uint256[] amounts; } - function load(uint electionId, address voter, uint256 precinct) internal pure returns (Data storage self) { - bytes32 s = keccak256(abi.encode("io.synthetix.governance.Ballot", electionId, voter, precinct)); + function load(uint256 electionId, address voter, uint256 chainId) internal pure returns (Data storage self) { + bytes32 s = keccak256(abi.encode("io.synthetix.governance.Ballot", electionId, voter, chainId)); assembly { self.slot := s } @@ -204,7 +204,7 @@ library Council { address councilToken; SetUtil.AddressSet councilMembers; mapping(address => uint) councilTokenIds; - uint currentElectionId; + uint256 currentElectionId; } function load() internal pure returns (Data storage store) { bytes32 s = _SLOT_COUNCIL_STORAGE;