Case Study — Voting Contract App
3.3 — Case Study: Voting Contract App
Recall first. A voting rule needs three things: a set of candidates, a count for each candidate, and a way to reject a second vote from the same address. Which Solidity features from 3.2 express those three requirements?
Source boundary: syllabus bridge, not a book program
The book mentions that Ethereum contracts can be used for voting from a valid address in the introduction to Chapter 14. It also discusses DAO proposals, stakeholder consensus, contractors, and voting in Chapter 5.4.3. However, the supplied book extract does not provide the complete voting-contract application below.12
This note is therefore an explicit syllabus bridge/book gap. The contract is a small educational synthesis of book-covered concepts—struct, mapping, dynamic array, modifier, event, require, functions, msg.sender, and view—not a claim that the book supplied this exact application.
Requirements
Build the smallest useful on-chain voting exercise:
- Store candidate names and vote counts.
- Let an address vote for one valid candidate.
- Reject an invalid candidate index.
- Reject a second vote from the same address.
- Emit a log when a vote succeeds.
- Expose read functions so a client can inspect the result.
Deliberate limits
This is not a production election system. It has no identity verification, Sybil resistance, secret ballots, proposal lifecycle, tie-breaking policy, vote-weight snapshot, administrator, or front-end. One address is treated as one voter, but one person may control multiple addresses. Those omissions are deliberate so that the syllabus concepts remain visible.
Educational contract — Solidity 0.5.1
pragma solidity ^0.5.1;
contract EducationalVoting {
struct Candidate {
string name;
uint voteCount;
}
Candidate[] public candidates;
mapping(address => bool) public hasVoted;
event VoteCast(address indexed voter, uint candidateIndex);
constructor(string memory firstName, string memory secondName) public {
candidates.push(Candidate(firstName, 0));
candidates.push(Candidate(secondName, 0));
}
modifier candidateExists(uint candidateIndex) {
require(candidateIndex < candidates.length, "invalid candidate");
_;
}
function vote(uint candidateIndex)
public
candidateExists(candidateIndex)
{
require(!hasVoted[msg.sender], "already voted");
hasVoted[msg.sender] = true;
candidates[candidateIndex].voteCount += 1;
emit VoteCast(msg.sender, candidateIndex);
}
function candidateCount() public view returns (uint) {
return candidates.length;
}
function leadingCandidate()
public
view
returns (uint index, uint votes)
{
require(candidates.length > 0, "no candidates");
index = 0;
votes = candidates[0].voteCount;
for (uint i = 1; i < candidates.length; i++) {
if (candidates[i].voteCount > votes) {
index = i;
votes = candidates[i].voteCount;
}
}
}
}
Compiler note. Select a compatible
0.5.xcompiler in Remix. Modern Solidity may require syntax changes; do not silently compile this old example with an incompatible compiler. The version choice follows the book’spragma solidity ^0.5.1discussion.3
Read the contract as a data model
| Requirement | Solidity element | Why |
|---|---|---|
| Candidate record | struct Candidate | name and count stay together |
| Candidate collection | Candidate[] candidates | preserves candidate indexes/order |
| One-vote flag | mapping(address => bool) hasVoted | direct lookup by caller address |
| Valid index rule | candidateExists modifier | reusable precondition |
| Failure | require | rejected calls do not apply the state change |
| Audit/UI signal | VoteCast event | external clients can listen to the log |
| Caller identity | msg.sender | identifies the address submitting the vote |
| Result query | view functions | read without a state-changing action |
Constructor
Deployment supplies two names. The constructor pushes two Candidate records into the dynamic storage array. It runs once, so it is a suitable place for initial setup.3
vote
The modifier first rejects an out-of-range candidate index. The function then checks the caller’s mapping entry, marks the caller as having voted, increments the selected record, and emits an event. A failed require stops execution; the attempted state change is not committed.3
The order is important: validate before changing state. In a larger contract, checks-effects-interactions is also a useful safety discipline, but this small example performs no external call.
leadingCandidate
The function scans the array and returns the first candidate with the greatest count. This is an O(n) educational scan. It leaves ties unresolved by policy: the first candidate wins the returned index. A real application must state its tie rule explicitly.
Worked example — trace three accounts
Deploy with "Alice" and "Bob":
candidates[0] = Alice, 0
candidates[1] = Bob, 0
Suppose three different accounts call:
| Call | Mapping effect | Candidate counts |
|---|---|---|
Account A: vote(1) | hasVoted[A] = true | Alice 0, Bob 1 |
Account B: vote(0) | hasVoted[B] = true | Alice 1, Bob 1 |
Account A: vote(0) | rejected by require | Alice 1, Bob 1 |
The third call fails because the address, not the candidate, identifies the previous voter. The failed call does not increment Alice and does not emit VoteCast.
If Account C calls vote(4), the modifier fails before the vote body runs because the array contains only indexes 0 and 1.
What the app demonstrates—and what it does not
Demonstrates
- A struct groups related state.
- A dynamic array stores an ordered set of candidates.
- A mapping provides direct caller-based eligibility state.
- A modifier reuses a validation rule.
requirerejects invalid calls.- An event exposes successful activity to an external client.
- A transaction changes state;
viewfunctions inspect it.
Does not demonstrate
- Identity: an address is not a verified human identity.
- Secret voting: votes and state are observable on a public chain.
- One-person-one-vote: a user may create multiple addresses.
- Governance completeness: real DAOs need proposal creation, voting windows, quorum, execution rules, and treasury controls.
- Correctness of the election: code cannot repair a bad candidate list, compromised account, or faulty deployment.
- Scalability: a loop over a growing array consumes more gas.
Exercise
- Why are both
Candidate[]andmapping(address => bool)needed? - What happens if the same address calls
vote(0)and thenvote(1)? - What is the result of a tie in this implementation?
- Which operation creates a transaction:
candidateCount()orvote(0)? Explain. - Add a requirement in words that would prevent voting after a deadline. What additional state would be needed?
- Name one reason this contract cannot provide a secure public election.
Revealed answers
- The array enumerates candidates and stores their records; the mapping performs a direct per-address “already voted?” check.
- The first call succeeds and sets the mapping entry. The second call reverts, so the second candidate’s count is unchanged.
leadingCandidatereturns the first candidate among tied candidates because it updates only when a strictly greater count is found. This is a design choice, not a universal voting rule.vote(0)changes storage and must be submitted as a transaction with gas.candidateCount()isview, so it can normally be read without a state-changing transaction.- Add a state variable such as
uint votingEndsAtand requirenow < votingEndsAtin a version-compatible implementation. The deadline itself must be set and interpreted carefully. - Any of: no identity/Sybil resistance, no secret ballot, observable votes, no protection for compromised keys, or no governance/audit process around deployment.
Exam lens
If asked to design a voting contract, write the data model first:
Candidate { name, voteCount }
Candidate[] candidates
mapping(address => bool) hasVoted
Then explain the state transition:
valid candidate?
→ caller has not voted?
→ mark caller
→ increment candidate
→ emit event
Mention that the full app is a syllabus bridge: the book supplies the component concepts and indirectly mentions voting, but not this complete program.12
Likely marks: structure/pragma, constructor, array and struct, mapping, modifier and require, event, state-changing vote function, read function, and one limitation.
Common traps:
- Checking only the candidate index does not prevent duplicate voting.
- An event is a log, not the authoritative state variable.
- A public address is not proof of a unique person.
- A
viewfunction is not automatically “free” in every off-chain application context; a client may still pay costs for a transaction wrapper, but the direct read does not change chain state.
Rapid revision
- Can I write the candidate struct, array, and voter mapping from memory?
- Can I explain the modifier and both
requirechecks? - Can I trace one successful vote and one reverted vote?
- Can I distinguish event logs from stored state?
- Can I state three production limitations?
- Can I explain why this is a syllabus bridge rather than a supplied book app?
Key takeaways
- The educational app combines the book’s Solidity primitives into a small voting state machine.
Candidate[]enumerates records;mapping(address => bool)enforces one vote per address.- Validation happens before the state update; successful votes emit an event.
- The implementation is not a real-world election: identity, secrecy, Sybil resistance, lifecycle, and governance are outside its scope.
- The book mentions voting but does not supply this complete app; label the bridge honestly in an exam answer or project report.
Sources
Footnotes
-
Blockchain Technology (converted book extract), Ch. 14 introduction, Markdown lines 8375–8377: smart contracts can be used for voting from a valid address. ↩ ↩2
-
Blockchain Technology (converted book extract), Ch. 5, §5.4.3, Markdown lines 4022–4039: DAO consensus, proposals, contractors, and voting. ↩ ↩2
-
Blockchain Technology (converted book extract), Ch. 14, §§14.1–14.17, Markdown lines 8367–8811: Solidity contracts, constructors, arrays, mappings, modifiers, events, functions, and errors. ↩ ↩2 ↩3