Private Voting (Module 12) — Votes are encrypted; tallies hidden until finalization
Treasury Management — Proposals to spend DAO funds
Frontend Integration (Module 10) — Full dApp interface
This is a challenging project that combines encrypted types, operations, ACL, conditional logic, decryption, and frontend integration.
Architecture Note: This lesson teaches a two-contract architecture (GovernanceToken + ConfidentialDAO) with weighted voting for educational purposes — it demonstrates cross-contract ACL, interface patterns, and advanced FHE composition. The reference implementation in contracts/ConfidentialDAO.sol uses a simplified monolithic architecture (single contract, unweighted votes) that is easier to test and deploy. Both approaches are valid; the two-contract version is the “stretch goal” for students who want a deeper challenge.
We use a simplified version of the confidential ERC-20 from Module 11. The DAO contract needs to read token balances for vote weighting, so we must set up cross-contract ACL.
// In GovernanceToken: allow the DAO contract to read balancesfunction grantDAOAccess(address dao) public { FHE.allow(_balances[msg.sender], dao);}
When a user wants to vote, they first grant the DAO contract access to their token balance. The DAO can then use the encrypted balance as the vote weight.
Unlike Module 12’s simple Yes/No voting (each vote = 1), the DAO uses weighted voting where your vote power equals your token balance:
function vote(uint256 proposalId, externalEbool encryptedVote, bytes calldata inputProof) external { // Get the voter's token balance (DAO must have ACL access) euint64 weight = governanceToken.balanceOf(msg.sender); ebool voteYes = FHE.fromExternal(encryptedVote, inputProof); euint64 zero = FHE.asEuint64(0); euint64 yesWeight = FHE.select(voteYes, weight, zero); euint64 noWeight = FHE.select(voteYes, zero, weight); p.yesVotes = FHE.add(p.yesVotes, yesWeight); p.noVotes = FHE.add(p.noVotes, noWeight);}
The voter’s entire token balance is used as the vote weight. If they vote Yes, their full balance is added to yesVotes and 0 to noVotes (and vice versa).
This is a new concept. The DAO contract needs to read the voter’s token balance, but the balance is encrypted and ACL-protected.Setup flow:
1
User holds governance tokens
Encrypted balance stored in the GovernanceToken contract.
2
User grants DAO access
Before voting, user calls:
governanceToken.grantDAOAccess(daoAddress)
This executes: FHE.allow(_balances[msg.sender], daoAddress)
3
DAO reads balance
Now when user votes, the DAO contract can read:
governanceToken.balanceOf(msg.sender)
and get the euint64 balance.
4
DAO uses balance as vote weight
The DAO uses this balance as the vote weight in the vote() function.
In the GovernanceToken contract:
function grantDAOAccess(address dao) public { require(_initialized[msg.sender], "No balance"); FHE.allow(_balances[msg.sender], dao);}// balanceOf needs to be callable by the DAO (not just msg.sender)function balanceOf(address account) public view returns (euint64) { return _balances[account];}
// Anyone can fund the treasuryreceive() external payable { emit TreasuryFunded(msg.sender, msg.value);}// Approved proposals transfer from treasurypayable(p.recipient).transfer(p.amount);
The treasury balance is public (ETH balance is always visible on-chain). This is a design choice — you could track an “encrypted budget” separately if needed.
If a user transfers tokens after voting on one proposal, they could receive tokens and vote again (on the same proposal, from a different address that now holds the tokens). The _hasVoted mapping prevents the same address from voting twice, but not the same tokens from being used twice.
Production solution: Snapshot balances at proposal creation time. This requires additional data structures.
For a trustless design, consider using FHE.makePubliclyDecryptable() so anyone can verify vote tallies, and add on-chain execution logic based on the decrypted results.