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
| Layer | Contract | What it does |
|---|---|---|
| Wallet | In-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. |
| Identity | AttesterNFT | Soulbound. Held by trusted attesters, who are also the five tally keyholders. |
| Identity | CitizenNFT | Soulbound credential for one verified resident. Minted and revoked by attestation. |
| Voting | SignUpTokenGatekeeper | Lets only CitizenNFT holders join the voter set, each credential once. |
| Voting | MACI core | The sign-up state tree and the factory that deploys a Poll per vote. |
| Voting | Poll + Tally (per vote) | Holds the encrypted ballots, verifies the proofs of correct counting. |
| Governance | Governor | Creates proposals, deploys polls, reads the tally, decides pass or fail. |
| Governance | Timelock | Queues 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.
| Step | What happens |
|---|---|
| Open session | A tally session is opened from the admin dashboard. |
| Submit shares | At least three of five keyholders submit their shares. |
| Reconstruct | The key is rebuilt in memory for about ten minutes, used, then wiped. |
| Prove | The coordinator generates zero-knowledge proofs that messages were processed and counted correctly. |
| Verify onchain | The 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
| Property | Guaranteed by |
|---|---|
| One real person, one vote | CitizenNFT plus the SignUp gatekeeper |
| Ballots nobody can read | MACI encryption to the coordinator key |
| No vote buying or coercion | MACI receipt-freeness and key change |
| No single operator can decrypt | 3-of-5 Shamir key federation |
| The count cannot lie | Zero-knowledge tally proof verified onchain |
| Safe, transparent execution | Governor plus Timelock delay |
| A citizen never touches the chain | ERC-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.