Casting a Vote
Create the processors for each of your instructions
To cast a vote, your program needs to do three things: verify the voter is eligible (the poll is active and they haven't already voted), record their vote by creating a VoteRecord account, and increment the vote count on the chosen PollOption.
The process_cast_vote handler needs access to the following accounts:
- The voter's account, which serves as both the signer and payer.
- The poll account, to validate that the voting window is open and the poll hasn't been closed early.
- The poll option account, which holds the running vote count for the chosen option.
- The vote record account, a PDA that serves as proof the voter has already cast their ballot.
- The system program account, required to create the vote record account.
Extracting and validating accounts
Start by extracting the accounts and confirming the voter has signed the transaction:
pub fn process_cast_vote(
program_id: &Pubkey,
accounts: &[AccountInfo],
option_id: u8,
) -> ProgramResult {
let acc_iter = &mut accounts.iter();
let voter_info = next_account_info(acc_iter)?; // signer / payer
let poll_info = next_account_info(acc_iter)?; // Poll PDA
let poll_option_info = next_account_info(acc_iter)?; // PollOption PDA
let vote_record_info = next_account_info(acc_iter)?; // VoteRecord PDA (new)
let system_info = next_account_info(acc_iter)?; // System Program
if !voter_info.is_signer {
return Err(ProgramError::MissingRequiredSignature);
}Validating the poll
Next, load the poll account and run a few checks before accepting the vote.
First, you need to confirm that the poll is owned by your program, deserialize it, and call assert_valid() to verify the discriminator, the same pattern you used in process_create_poll:
if poll_info.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let poll: Poll = {
let data = poll_info.try_borrow_data()?;
Poll::try_from_slice(&data).map_err(|_| ProgramError::InvalidAccountData)?
};
poll.assert_valid()?;Then check that the poll hasn't been closed early and that the current slot falls within the voting window:
if poll.closed_early {
return Err(PollError::PollAlreadyClosed.into());
}
let clock = Clock::get()?;
let current_slot = clock.slot;
if current_slot < poll.start_slot || current_slot > poll.end_slot {
return Err(PollError::VotingWindowClosed.into());
}Preventing double votes
Recall that when you modeled the VoteRecord as a PDA, you noted that its address encodes both the poll and the voter. That property is what makes double-vote prevention cheap. The PDA seeds are:
- A static string:
vote - The poll account's public key
- The voter's public key
Because this combination is unique per (poll, voter) pair, only one VoteRecord can ever exist for a given voter on a given poll. If the account already exists (meaning data_is_empty() returns false), the voter has already cast their ballot.
let vote_record_seeds = &[b"vote", poll_info.key.as_ref(), voter_info.key.as_ref()];
let (vote_record_pda, vote_record_bump) =
Pubkey::find_program_address(vote_record_seeds, program_id);
if *vote_record_info.key != vote_record_pda {
return Err(ProgramError::InvalidSeeds);
}
// If the account already exists, this voter has already voted
if !vote_record_info.data_is_empty() {
return Err(ProgramError::AccountAlreadyInitialized);
}Validating the poll option
Before incrementing its vote count, confirm that the PollOption account is owned by your program, that its discriminator passes, and that it actually belongs to this poll and matches the option_id the voter selected.
The last two checks catch a class of attack where a client supplies a PollOption from a different poll or swaps in an option with a different ID to redirect votes:
if poll_option_info.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let mut poll_option: PollOption = {
let data = poll_option_info.try_borrow_data()?;
PollOption::try_from_slice(&data).map_err(|_| ProgramError::InvalidAccountData)?
};
poll_option.assert_valid()?;
if poll_option.parent_poll != *poll_info.key {
return Err(ProgramError::InvalidArgument);
}
if poll_option.option_id != option_id {
return Err(ProgramError::InvalidArgument);
}Recording the vote and updating the tally
With all checks passing, you can now create the VoteRecord account, initialize it, and increment the vote count on the PollOption. The VoteRecord is a PDA, so you use invoke_signed here, same as when you created the Poll and PollOption accounts:
let lamports = Rent::get()?.minimum_balance(VoteRecord::SIZE);
invoke_signed(
&solana_system_interface::instruction::create_account(
voter_info.key,
vote_record_info.key,
lamports,
VoteRecord::SIZE as u64,
program_id,
),
&[
voter_info.clone(),
vote_record_info.clone(),
system_info.clone(),
],
&[&[
b"vote",
poll_info.key.as_ref(),
voter_info.key.as_ref(),
&[vote_record_bump],
]],
)?;
let vote_record = VoteRecord::new(*poll_info.key, *voter_info.key, option_id);
vote_record.serialize(&mut &mut vote_record_info.data.borrow_mut()[..])?;Finally, increment the vote count on the chosen option using checked_add to guard against overflow, and write the updated struct back:
poll_option.vote_count = poll_option
.vote_count
.checked_add(1)
.ok_or(ProgramError::ArithmeticOverflow)?;
poll_option.serialize(&mut &mut poll_option_info.data.borrow_mut()[..])?;
msg!(
"Event: vote_cast - Poll={}, Voter={}, OptionId={}",
poll_info.key,
voter_info.key,
option_id
);
Ok(())
}Now that you have implemented the vote handler, in the next section you will implement the last instruction handler: closing a poll early.
Last updated on