President Elector

First Flight #24
Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Invalid

Voters who cast their votes via `rankCandidatesBySig` will not have their votes registered due to erroneous internal function

Summary

Due to the wrong signer address passed to the internal function _rankCandidates within rankCandidatesBySig, voters who call the function rankCandidatesBySig to cast their votes will not be able to register their votes, making the function rankCandidatesBySig is not usable as intended

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

Vulnerability Details

The internal function _rankCandidates in the rankCandidatesBySig was found taking in input parameter using signer address which is the address of the sponsor instead of the voter's address who calls the function to cast their votes. This causes voters won't be able to register their votes using this function.

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);
}

Proof of Concept:

  1. Setup a HelperContract for ease of PoC to interact with various functions in contract RankedChoice and EIP712 during test case.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {RankedChoice} from "./RankedChoice.sol";
contract HelperContract is RankedChoice {
constructor(address[] memory voters) RankedChoice(voters) {}
function getAdjustedTypeHashMessage(address[] memory orderedCandidates) public view returns(bytes32) {
// it is best to note that the original `TYPEHASH` in the `RankedChoice` contract is also found not appropriately hashed as the function `rankCandidates` takes in an array of address not an array of uint256. Though this step doesn't affect the recovery of signature, for completeness purpose, the following PoC will use the corrected `TYPEHASH` as `keccak256("rankCandidates(address[])")` instead.
bytes32 TYPEHASH = keccak256("rankCandidates(address[])");
bytes32 structHash = keccak256(abi.encode(TYPEHASH, orderedCandidates));
bytes32 hash = _hashTypedDataV4(structHash);
return hash;
}
}

2.Modify the test/RankedChoiceTest.t.sol by adding in more specific setup for use in PoC test cases

contract RankedChoiceTest is Test {
address[] voters;
address[] candidates;
// tapout variables to use along with various PoC
address[] voters_poc;
address[] candidates_poc;
address[] orderedCandidates_sponsor = new address[]();
address sponsor;
uint256 sponsorKey;
bytes sponsorSignature;
uint256 constant MAX_VOTERS = 100;
uint256 constant MAX_CANDIDATES = 20;
uint256 constant VOTERS_ADDRESS_MODIFIER = 100;
uint256 constant CANDIDATES_ADDRESS_MODIFIER = 200;
RankedChoice rankedChoice;
HelperContract helperContract;
address[] orderedCandidates;
function setUp() public {
for (uint256 i = 0; i < MAX_VOTERS; i++) {
voters.push(address(uint160(i + VOTERS_ADDRESS_MODIFIER)));
}
rankedChoice = new RankedChoice(voters);
for (uint256 i = 0; i < MAX_CANDIDATES; i++) {
candidates.push(address(uint160(i + CANDIDATES_ADDRESS_MODIFIER)));
}
// tapout new array of voters for POC purpose with the first array item is the sponsor address with a known key
address[] memory newVoters = new address[]();
(sponsor, sponsorKey) = makeAddrAndKey("Sponsor");
newVoters[0] = sponsor;
for (uint256 i = 0; i < voters.length; i++) {
newVoters[i + 1] = voters[i];
}
voters_poc = newVoters;
// tapout new array of candidates for PoC test purpose and set max at 10 as stated in contract `RankedChoice`
for (uint256 i = 0; i < 10; i++) {
candidates_poc.push(address(uint160(i + CANDIDATES_ADDRESS_MODIFIER)));
}
// tapout sponsor's orderedCandidates
orderedCandidates_sponsor[0] = candidates_poc[0];
orderedCandidates_sponsor[1] = candidates_poc[1];
orderedCandidates_sponsor[2] = candidates_poc[2];
// for ease of interating with functions tight with EIP712 and RankedChoice contracts, tapout a helper contract to use for various PoC test cases
helperContract = new HelperContract(voters_poc);
}
// add modifier to setup a valid sponsor signature
modifier tapoutSponsorSignature() {
vm.prank(sponsor);
bytes32 hash = helperContract.getAdjustedTypeHashMessage(orderedCandidates_sponsor);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(sponsorKey, hash);
bytes memory signature = abi.encodePacked(r, s, v);
sponsorSignature = signature;
_;
}
function test_audit_wrongSignerAddressUsedInRankCandidatesBySig() public tapoutSponsorSignature {
address voter = voters_poc[1];
vm.startPrank(voter);
helperContract.rankCandidatesBySig(orderedCandidates_sponsor, sponsorSignature);
address[] memory voterVote = helperContract.getUserCurrentVote(voter);
vm.stopPrank();
assertEq(voterVote, orderedCandidates_sponsor);
}

3.Run the test with forge test --match-test test_audit_wrongSignerAddressUsedInRankCandidatesBySig -vv
The test will show assertion failed with voter's orderedCandidates list is empty, which proves that voters can't cast their votes when interacting with rankCandidatesBySig.

Console log as follow:

$ forge test --match-test test_audit_wrongSignerAddressUsedInRankCandidatesBySig -vv [15:15:26]
[⠊] Compiling...
[⠰] Compiling 1 files with Solc 0.8.24
[⠔] Solc 0.8.24 finished in 1.32s
Compiler run successful!
Ran 1 test for test/RankedChoiceTest.t.sol:RankedChoiceTest
[FAIL. Reason: assertion failed: [] != [0x00000000000000000000000000000000000000C8, 0x00000000000000000000000000000000000000C9, 0x00000000000000000000000000000000000000ca]] test_audit_wrongSignerAddressUsedInRankCandidatesBySig() (gas: 453853)
Logs:
voter's orderedCandidate length: 0
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 7.11ms (605.50µs CPU time)

Impact

Voters won't be able to cast their votes using rankCandidatesBySig. The function is not usable as intended.

Tools Used

Manual review with test

Recommendations

To change the input parameter used in the internal function from signer to msg.sender, so the caller's (which is the voter) orderedCandidate list get registered as their own vote when using a sponsored signature to cast the votes.

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);
+ _rankCandidates(orderedCandidates, mgs.sender);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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