← All posts
Jun 2026 · Field notes · Netizen Labs

Anatomy of a private vote

A citizen in Röbel votes by opening an app and tapping Yes, No, or Abstain. That is the whole experience. Underneath that tap is a chain of smart contracts and cryptography built so the vote stays private, the count stays provable, and no single operator can read a ballot or fake a result.

This is the entire path, from getting a wallet to a passed proposal executing onchain, with every contract named. It all lives on Base mainnet, so you can verify it instead of taking our word for it.

The cast

LayerContractWhat it does
WalletIn-app smart account (ERC-4337)A wallet created from an email login. A paymaster sponsors gas, so the citizen never holds ETH or a seed phrase.
IdentityAttesterNFTSoulbound. Held by trusted attesters, who are also the five tally keyholders.
IdentityCitizenNFTSoulbound credential for one verified resident. Minted and revoked by attestation.
VotingSignUpTokenGatekeeperLets only CitizenNFT holders join the voter set, each credential once.
VotingMACI coreThe sign-up state tree and the factory that deploys a Poll per vote.
VotingPoll + Tally (per vote)Holds the encrypted ballots, verifies the proofs of correct counting.
GovernanceGovernorCreates proposals, deploys polls, reads the tally, decides pass or fail.
GovernanceTimelockQueues a passed proposal, enforces a delay, then executes it.

Phase 0: a wallet nobody has to understand

Opening the app creates an ERC-4337 smart account from a normal login. There is no seed phrase and no token to buy. Every transaction this account sends is routed through a paymaster that pays the gas, so the citizen never touches a blockchain interaction directly, even though real ones are happening underneath.

Phase 1: becoming a citizen

Citizenship is a credential, not a checkbox. To mint a CitizenNFT to your smart account, one AttesterNFT holder and one existing citizen both have to vouch for you. The result is soulbound: it cannot be sold or transferred, and the same one-plus-one threshold can revoke it. That is what makes membership chosen rather than coerced, and what guarantees one real person holds one credential.

Phase 2: joining the voter set

Before you can vote, your smart account signs up to MACI. The client derives a MACI voting keypair deterministically from your wallet, so the same person regenerates the same voting identity on any device. The call to sign up passes through the SignUpTokenGatekeeper, which checks you hold a CitizenNFT and that this exact credential has not already signed up. Your MACI public key then becomes one leaf in the sign-up tree. This is where one person, one vote is enforced in cryptography, not in a database.

Phase 3: opening a proposal

A proposal is created on the Governor, which deploys a fresh Poll through MACI. The Poll is configured with a voting window and the coordinator public key that ballots will be encrypted to. The client always reads that key from the Poll itself at vote time, so a rotated key can never silently void a ballot.

Phase 4: casting a private vote

When a citizen taps an option, the client builds a vote command, signs it with the voter's MACI key, and encrypts it to the coordinator key. It is submitted as a message to the Poll from the smart account, gasless. The ballot is now onchain as ciphertext that nobody can read, not even the operator.

MACI also makes the vote receipt-free: a voter can quietly override an earlier choice, so a vote buyer can never prove how anyone finally voted. Coercion has nothing to grab.

Phase 5: counting it

When voting closes, the ballots have to be decrypted and tallied, and here is the part most systems get wrong. The coordinator key that can read votes does not live on a server. It is split three-of-five across the five attester keyholders using Shamir secret sharing.

StepWhat happens
Open sessionA tally session is opened from the admin dashboard.
Submit sharesAt least three of five keyholders submit their shares.
ReconstructThe key is rebuilt in memory for about ten minutes, used, then wiped.
ProveThe coordinator generates zero-knowledge proofs that messages were processed and counted correctly.
Verify onchainThe Tally contract accepts the result only if the proof verifies.

A wrong count is not disputed afterward. It is impossible to submit, because the proof would not verify. Between tallies the key does not exist anywhere, so there is nothing to steal, subpoena, or leak.

Phase 6: executing it

The Governor reads the verified tally and applies the quorum and threshold to decide pass or fail. A passed proposal is queued in the Timelock, which enforces a minimum delay before anything runs. During that window the exact pending action is public and can be cancelled. Once the delay elapses, the action executes onchain and becomes permanent.

What each layer guarantees

PropertyGuaranteed by
One real person, one voteCitizenNFT plus the SignUp gatekeeper
Ballots nobody can readMACI encryption to the coordinator key
No vote buying or coercionMACI receipt-freeness and key change
No single operator can decrypt3-of-5 Shamir key federation
The count cannot lieZero-knowledge tally proof verified onchain
Safe, transparent executionGovernor plus Timelock delay
A citizen never touches the chainERC-4337 smart account plus paymaster

Verify, do not trust

Every contract above is live and verified on Base. Anyone can check that the Governor points at the federated coordinator key, that only the five attesters can hold a tally share, and that each result carries a proof. That is the whole point: a town should not have to trust a founder, a company, or a single machine. It should be able to check.

This is the stack that runs Röbel today, and the same stack any community can deploy.