Summary
Although each voter is intended to cast only one vote, the lack of checks to enforce this can lead to accidental overwriting of their previous choice. This unintended overwrite may affect the voting outcome by altering the recorded vote in ways not intended, potentially causing inaccuracies in the final results.
Vulnerability Details
The RankedChoice::rankCandidates and RankedChoice::rankCandidatesBySig function does not verify if a voter has already cast a vote. As a result, a voter could accidentally vote again, potentially altering their vote to different candidates than their original choice.
...
function rankCandidates(address[] memory orderedCandidates) external {
_rankCandidates(orderedCandidates, msg.sender);
}
function rankCandidatesBySig(address[] memory orderedCandidates, bytes memory signature)
external
{
bytes32 structHash = keccak256(abi.encode(TYPEHASH, orderedCandidates));
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, signature);
_rankCandidates(orderedCandidates, signer);
}
...
Impact
Voters can accidentally overwrite their previous vote, leading to potential discrepancies in the voting results and inaccuracies in the final tally.
PoC
In the RankedChoiceTest.t.sol file, add the following code:
...
address alice = makeAddr("alice");
...
function testVoteOverwrite() public {
orderedCandidates = [candidates[0], candidates[1], candidates[2]];
vm.prank(voters[0]);
rankedChoice.rankCandidates(orderedCandidates);
orderedCandidates = [candidates[0], candidates[1], alice];
vm.prank(voters[0]);
rankedChoice.rankCandidates(orderedCandidates);
assertEq(rankedChoice.getUserCurrentVote(voters[0]), orderedCandidates);
}
Tools Used
VS Code, Manual Review
Recommendations
Add a check to ensure that each voter can only vote once.
contract RankedChoice is EIP712{
...
// add a new mapping
+ mapping(address => bool) private hasVoted;
...
function rankCandidates(address[] memory orderedCandidates) external {
// check if address has voted
+ require(!hasVoted[msg.sender], "Address already voted");
_rankCandidates(orderedCandidates, msg.sender);
}
function rankCandidatesBySig( // @note looks OK
address[] memory orderedCandidates, bytes memory signature)
external
{
bytes32 structHash = keccak256(abi.encode(TYPEHASH, orderedCandidates));
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, signature);
+ require(!hasVoted[signer], "Address already voted");
_rankCandidates(orderedCandidates, signer);
}
...
}