President Elector

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

Signature Replay Vulnerability in RankedChoice Contract's rankCandidatesBySig Function

Summary

The rankCandidatesBySig function is vulnerable to signature replay attacks due to the absence of proper nonce management and other essential EIP-712 safeguards. This vulnerability could allow malicious actors to reuse valid signatures, potentially manipulating the voting process in favor of certain candidates.

Vulnerability Details

2024-09-president-elector/src/RankedChoice.sol at main · Cyfrin/2024-09-president-elector (github.com)

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);
}
  • Lack of Nonce: The function does not implement a nonce system, allowing signatures to be reused multiple times.

  • Missing ChainID: The absence of chainId in the signed message enables cross-chain replay attacks.

  • No Expiration Mechanism: Without a deadline or expiration timestamp, signatures remain valid indefinitely.

  • Incomplete EIP-712 Implementation: The contract does not fully adhere to the EIP-712 standard, missing crucial elements like a properly structured domain separator.

These vulnerabilities collectively undermine the integrity of the signature-based voting system, potentially allowing attackers to:

  • Replay votes multiple times

  • Use signatures across different blockchain networks

  • Exploit old signatures that should no longer be valid

Impact

Use of old signatures in favor of particular contestant will compromise the fairness and integrity of the voting system, potentially leading to illegitimate election outcomes.

Tools Used

Manual Review

Recommendations

It can be fixed by one of the following ways:

Implement Nonce System:

  • Add a nonce for each voter: mapping(address => uint256) private _nonces;

  • Include nonce in the signed message structure.

  • Verify and increment nonce in rankCandidatesBySig

    Add ChainID to Signed Message:

  • Include block.chainid in the message hash to prevent cross-chain replays.

    Implement Expiration Mechanism:

  • Add a deadline parameter to the signed message.

  • Check block.timestamp against the deadline in rankCandidatesBySig.

    Fully Implement EIP-712:

  • Create a proper domain separator including contract name, version, chainId, and contract address.

  • Use structured data hashing as per EIP-712 specification.

function rankCandidatesBySig(
address[] memory orderedCandidates,
uint256 nonce,
uint256 deadline,
bytes memory signature
) external {
require(block.timestamp <= deadline, "Signature expired");
require(_nonces[msg.sender] == nonce, "Invalid nonce");
bytes32 structHash = keccak256(abi.encode(
VOTE_TYPEHASH,
keccak256(abi.encodePacked(orderedCandidates)),
nonce,
deadline,
block.chainid
));
bytes32 hash = _hashTypedDataV4(structHash);
address signer = ECDSA.recover(hash, signature);
require(_isInArray(VOTERS, signer), "Invalid voter");
_nonces[signer]++;
_rankCandidates(orderedCandidates, signer);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year 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.