Hi Andrew,
As promised, I have copied my code below. I’m aware that there are things that I could do to make the bytecode smaller (e.g. I have duplicated inherited functions in my contract, I could get rid of the modifiers and just put require statements in the functions). However, I decided that it’s better to leave the contract as-is and get you to give me the full feedback on all the things I’ve done wrong and how it can be improved. I would rather be flayed on the internet and learn a lot quickly than stumble along by myself at a slow pace. If that’s too much work, just give me some basic guidelines and I’ll take it from there.
I’m working with a friend who is going to be handling the web3.js side, but he has only just started to get involved. Once we start to get a better understanding of how the front end will interact with the contract, I’m sure we’ll end up changing a lot.
FYI, the plan is to implement a contract factory design so that the application creates a new contract for each election.
Once again, thanks for all of your help. It’s all gold.
Your biggest fan,
Craig
pragma solidity ^0.7.0;
import "../node_modules/@openzeppelin/contracts/presets/ERC1155PresetMinterPauser.sol";
contract Election is ERC1155PresetMinterPauser {
using SafeMath for uint256;
/// @dev numberOfOffices will ultimately be passed in to the constructor function by the contract factory.
/// @dev As the name suggests, it details how many offices will be contested in the election.
uint private numberOfOffices;
/// @dev We need to track total number of candidates in the election so we can break down the Candidate structs into
/// @dev individual arrays at the end of the election in the returnVotesAndDetermineWinner function.
uint private totalCandidates;
/// @dev electionName will be passed in to the constructor function by the contract factory.
/// @dev electionName is the name of the particular election
string public electionName;
/// @dev State are different states for controlling which functions can be used during different phases of the election
enum State { Populating, Voting, Ended }
/// @dev Declaring a gobal State variable for use in the contract
State private state;
/// @dev uint array for the officeId (e.g. 0 is for president, 1 is for VP, 2 is for secretary, etc.)
/// @dev for our purposes, we will populate the array with sequential numbers (e.g. if 5 offices in election, array will be [0,1,2,3,4])
uint[] ids;
/// @dev uint array for number of tokens (aka votes) per each officeId.
/// @dev As every voter will receive 1 vote for each office, this will be 1 for each element (e.g. if 5 offices, array will be [1,1,1,1,1])
/// @dev The length of the array will be equal to the ids array (so that each officeId has a corresponding number of votes per voter)
uint[] amounts;
/// @dev struct to hold data on each candidate
/// @dev name (obvious), officeId is the office for which they are running, candidateAddress is their Ethereum address,
/// @dev arrayIndex is their index number in the officeToCandidate mapping, votes is their number of votes(*** 'votes' probably not needed)
struct Candidate {
string name;
uint officeId;
address candidateAddress;
uint arrayIndex;
uint votes;
}
/// @dev mapping to store the candidates for each office. E.g. the initial uint is officeId so officeToCandidate[1] will resolve to an array of
/// @dev Candidate structs associated with officeId 1.
mapping (uint => Candidate[]) public officeToCandidate;
/// @dev mapping to indicate if a particular candidate is running for a particular office. Needed to ensure that
/// @dev voters can only use a particular token for the corresponding officeId. E.g. the initial uint is officeId so officeToCanddiate[1] will
/// @dev resolve to a mapping of addresses to bools associated with officeId 1. So, officeToCandidate[1][0x123ABC] will resolve to true or false
/// @dev which indicates if that address is or isn't running for that particular office
mapping (uint => mapping (address => bool)) private officeToCandidateToBool;
/// @dev modifier to determine if the contract is in the correct state (Populating, Voting, or Ended).
modifier inState(State _state)
{
require(state==_state, "Invalid State.");
_;
}
/// @dev modifier to determine if the message sender has Minter role
modifier isMinter()
{
require(hasRole(MINTER_ROLE, _msgSender()), "Caller is not a minter");
_;
}
/// @dev modifier to determine if the message sender has Pauser role
modifier isPauser()
{
require(hasRole(PAUSER_ROLE, _msgSender()), "Caller is not a pauser");
_;
}
/// @dev modifier to determine if the message sender has Admin role
modifier isAdmin()
{
require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller is not an admin");
_;
}
/// @dev constructor function that initializes some of the state variables using arguments and sets inital state to 'Populating'
/// @dev also passing a string to the ERC1155PresetMinterPauser parent contract. As we're not using a uri at the moment, I've set it to 'uri'.
/// @dev There's a loop to populate the ids and amounts arrays based on the number of offices in the election. If there are five offices in
/// @dev the election ids will be [0,1,2,3,4] and amounts will be [1,1,1,1,1].
/// @param _numberOfOffices is required so the contract knows how many offices are in the election
/// @param _electionName is the name of the election (e.g. election 2021)
constructor(uint _numberOfOffices, string memory _electionName) ERC1155PresetMinterPauser("uri")
{
state = State.Populating;
numberOfOffices = _numberOfOffices;
electionName = _electionName;
for (uint i; i < _numberOfOffices; i++) {
ids.push(i);
amounts.push(1);
}
}
/// @dev function to add candidates to contract which can only be done during while contract is in initial 'Populating' state and only
/// @dev by an admin.
/// @dev arguments are placed into a temporary Candidate struct which is then (along with the index position in the array) pushed
/// @dev into a Candidate array located in the officeToCandidate mapping. Also, the bool is set to 'true' in the officeToCandidateBool
/// @dev mapping for the designated _officeId (e.g. 0=Preseident, 1=VP, 2=Secretary) and _canddiateAddress.
/// @dev Lastly, the totalCandidates variable is incremented by one.
/// @param _candidateAddress is the Ethereum address for the canddiate to be added
/// @param _officeId is the number associated with a particular office in the election (e.g. 0=Preseident, 1=VP, 2=Secretary)
/// @param _name is the name of the candidate
function addCandidate(address _candidateAddress, uint _officeId, string memory _name)
public
isAdmin
inState(State.Populating)
{
Candidate memory tempCandidate;
tempCandidate.officeId = _officeId;
tempCandidate.name = _name;
tempCandidate.candidateAddress = _candidateAddress;
tempCandidate.arrayIndex = officeToCandidate[_officeId].length;
officeToCandidate[_officeId].push(tempCandidate);
officeToCandidateToBool[_officeId][_candidateAddress] = true;
totalCandidates = totalCandidates.add(1);
}
/// @dev function to remove candidates to contract which can only be done during while contract is in initial 'Populating' state
/// @dev and only by an admin.
/// @dev arguments are placed into a temporary Candidate struct which is then (along with the index position in the array) pushed
/// @dev into a Candidate array located in the officeToCandidate mapping. Also, the bool is set to 'true' in the officeToCandidateBool
/// @dev mapping for the designated _officeId (e.g. 0=Preseident, 1=VP, 2=Secretary) and _canddiateAddress.
/// @dev Lastly, the totalCandidates variable is incremented by one.
/// @param _officeId is the number associated with a particular office in the election (e.g. 0=Preseident, 1=VP, 2=Secretary)
/// @param _arrayIndex is the index position of the candidate in the officeToCandidate mapping array marked for removal
function removeCandidate(uint _officeId, uint _arrayIndex)
public
isAdmin
inState(State.Populating)
{
require (_arrayIndex < officeToCandidate[_officeId].length);
// set candidate address to false for this officeId
officeToCandidateToBool[_officeId][officeToCandidate[_officeId][_arrayIndex].candidateAddress] = false;
// copy last element into array index marked for deletion spot
officeToCandidate[_officeId][_arrayIndex] = officeToCandidate[_officeId][officeToCandidate[_officeId].length - 1];
// change Candidate struct to reflect new array position
officeToCandidate[_officeId][_arrayIndex].arrayIndex = _arrayIndex;
// pop off last element which was copied into
officeToCandidate[_officeId].pop();
totalCandidates = totalCandidates.sub(1);
}
/// @dev Previous to deployment of election contract, we will have created all of the Ethereum accounts for all of the voters and
/// @dev candidates. So, we will pass in the voter's Ethereum address and mint tokens in the quantity specified in the amounts array
/// @dev for each officeId in that array to that address. Only the Minter can use this function and only while in the 'Populating' state.
/// @dev ids is the array of officeIds (aka offices in the election).
/// @dev amounts is the array of number of tokens (aka votes) per officeId
/// @dev "0x00" is a placeholder because the function requires a data argument that we're not using
/// @param _voterAddress is the Ethereum address for a particular voter
function addVoterTokens(address _voterAddress)
public
isMinter
inState(State.Populating)
{
_mintBatch(_voterAddress, ids, amounts,"0x00");
}
/// @dev To remove tokens for all officeIds and amounts specified in the ids and amounts arrays.
/// @dev Exact opposite of addVoterTokens
/// @dev ids is the array of officeIds (aka offices in the election).
/// @dev amounts is the array of number of tokens (aka votes) per officeId
/// @param _voterAddress is the Ethereum address for a particular voter
function removeVoterTokens(address _voterAddress)
public
isMinter
inState(State.Populating)
{
_burnBatch(_voterAddress, ids, amounts);
}
/// @dev function used to transfer tokens from voters to candidates. Can be called by anyone when contract is in Voting state.
/// @dev This function can be paused by the Pauser to stop voting in case of an emergency.
/// @param _officeId is the number associated with a particular office in the election (e.g. 0=Preseident, 1=VP, 2=Secretary)
/// @param _candidateArrayIndex is the index position of the Candidate struct in the Candidate array in the officeToCandidate mapping
function vote(uint _officeId, uint _candidateArrayIndex)
public
whenNotPaused()
inState(State.Voting)
{
// check that the candidate being voted for is registered as a candidate for the particular office
require (officeToCandidateToBool[_officeId][officeToCandidate[_officeId][_candidateArrayIndex].candidateAddress] == true);
safeTransferFrom(_msgSender(), officeToCandidate[_officeId][_candidateArrayIndex].candidateAddress, ids[_officeId], amounts[_officeId], "0x00");
}
/// @dev fuunction that starts the election by changing the state from 'Populating' to 'Voting' which enables the vote function
/// @dev only admin can call this function and only when election is in the 'Populating' state
function startElection()
public
isAdmin
inState(State.Populating)
{
state = State.Voting;
}
/// @dev Function that determines the winner (or draw) for each office and returns the number of votes for each candidate.
/// @dev Can only be called by the admin and only when the election is in the 'Ended' state.
/// @dev Private function so that it can only be called wihtin this contract (by the endElection function)
/// @return returns six arrays (officeIds, candidateAddresses, arrayIndices, votes, winners, drawOrNots)
/// @return the values in the officeIds, candidateAddresses, arrayIndices, votes arrays line up
/// @return while the index values for the winners and drawOrNot arrays are the officeIds
function returnVotesAndDetermineWinners()
private
view
isAdmin
inState(State.Ended)
returns (uint[] memory, uint[] memory, uint[] memory, uint[] memory, bool[] memory)
{
// These are temporary memory arrays that exist solely to allow me to break down into single arrays to return
uint[] memory officeIds = new uint[](totalCandidates);
uint[] memory arrayIndices = new uint[](totalCandidates);
uint[] memory votes = new uint[](totalCandidates);
uint[] memory winners = new uint[](numberOfOffices);
bool[] memory drawOrNots = new bool[](numberOfOffices);
// A temporary index for populating the individual arrays in the nested loop below
uint tempCandidateIndex;
// Outer loop is for cycling through all of the offices in the election
for (uint office = 0; office < numberOfOffices; office = office.add(1))
{
// A temporary index for tracking the array index of the candidate with the highest number of tokens (votes)
uint currentWinnerIndex = 0;
// Inner loop is for cycling through all of the candidates running for the various offices
for (uint candidate = 0; candidate < officeToCandidate[office].length; candidate = candidate.add(1))
{
officeIds[tempCandidateIndex] = officeToCandidate[office][candidate].officeId; // Needed to identify candidate in officeToCandidate mapping
arrayIndices[tempCandidateIndex] = officeToCandidate[office][candidate].arrayIndex; //Needed to identify candidate in officeToCandidate mapping
votes[tempCandidateIndex] = balanceOf(officeToCandidate[office][candidate].candidateAddress,office); // Not needed? Can query directly using balanceOf?
// cycling through candidates votes to find the highest number
if (votes[tempCandidateIndex] > votes[currentWinnerIndex])
{
currentWinnerIndex = officeToCandidate[office][candidate].arrayIndex;
}
// if current number isn't higher than biggest number, check to see if they're equal
else if (votes[tempCandidateIndex] == votes[currentWinnerIndex])
{
drawOrNots[office] = true;
}
// Increment tempTotal candidates so the next candidate data will be placed in the right index in the various arrays
tempCandidateIndex = tempCandidateIndex.add(1);
}
// Highest number of votes at the end of a particular office loop must be the winner for that office (unless there's a draw)
winners[office] = currentWinnerIndex;
}
// return the various individual arrays
return (officeIds, arrayIndices, votes, winners, drawOrNots);
}
/// @dev function to end the election by changing state from 'Voting' to 'Ended'
/// @dev Also, calls returnVotesAndDetermineWinner to return number of votes and determine winner
/// @dev can only be called by the Admin and when the contract state is 'Voting'
/// @return returns six arrays (officeIds, candidateAddresses, arrayIndices, votes, winners, drawOrNots) passed from private function
/// @return the values in the officeIds, candidateAddresses, arrayIndices, votes arrays line up
/// @return while the index values for the winners and drawOrNot arrays are the officeIds
function endElection()
public
isAdmin
inState(State.Voting)
returns(uint[] memory, uint[] memory, uint[] memory, uint[] memory, bool[] memory)
{
state = State.Ended;
return returnVotesAndDetermineWinners();
}
}