Confidential DAO System
Overview
The Lunarys DAO System implements a fully confidential on-chain governance protocol where votes, voting power, and token balances remain encrypted throughout the entire governance lifecycle. Built on Zama's fhEVM, the system enables secure, private decision-making while maintaining verifiable outcomes.
The DAO consists of four core contracts working together:
- ConfidentialGovernanceToken - ERC7984 governance token with encrypted balances
- ConfidentialGovernor - Main governance logic with encrypted voting
- ConfidentialTimelock - Time-delayed execution for approved proposals
- ConfidentialTreasury - Manages DAO funds with confidential accounting
Architecture
The DAO system follows a hierarchical structure:
- Token Holders deposit ConfidentialGovernanceToken into the Governor
- ConfidentialGovernor manages proposal lifecycle and encrypted voting
- ConfidentialTimelock enforces delays and executes approved proposals
- ConfidentialTreasury holds and disburses DAO funds under timelock control
Key Features
Complete Vote Privacy
- Vote amounts encrypted as
euint64 - Vote direction (For/Against/Abstain) encrypted as
euint8 - Tallies computed homomorphically without decryption
- Final results only decrypted after voting ends
Flexible Governance
- Configurable voting parameters (delay, period, quorum)
- Proposal threshold to prevent spam
- Multi-call proposal execution (batch operations)
- Grace period for execution window
Secure Timelock
- Mandatory delay before execution
- Cancellation capability for emergencies
- Batch operation support
- Role-based access control
Confidential Treasury
- Encrypted fund management
- Proposal-controlled spending
- Transparent governance without exposing amounts
Governance Token
ConfidentialGovernanceToken
Standard: ERC7984 (Confidential ERC20)
Key Functions:
// Mint new governance tokens
function mint(
address to,
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external onlyRole(MINTER_ROLE) returns (euint64);
// Burn tokens
function burn(
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external returns (euint64);
// Confidential transfer
function confidentialTransfer(
address to,
euint64 encryptedAmount
) external returns (euint64);
Features:
- Fully ERC7984 compliant
- Encrypted balances and transfers
- Minter role for controlled supply
- Operator approvals for DeFi integrations
Governor Contract
Proposal States
enum ProposalState {
Pending, // Created, voting hasn't started
Active, // Currently accepting votes
AwaitingDecryption, // Voting ended, waiting for oracle
Defeated, // Failed quorum or vote count
Succeeded, // Passed, ready to queue
Queued, // Scheduled in timelock
Executed, // Successfully executed
Canceled, // Canceled by admin
Expired // Execution window passed
}
Vote Types
enum VoteType {
Against, // Vote against proposal
For, // Vote for proposal
Abstain // Abstain (counts toward quorum)
}
Creating Proposals
function propose(
address[] memory targets,
uint256[] memory values,
bytes[] memory calldatas,
string memory description
) external returns (uint256 proposalId);
Parameters:
targets: Array of contract addresses to callvalues: Array of ETH values to send (usually 0)calldatas: Array of encoded function callsdescription: Human-readable proposal description
Requirements:
- Caller must have sufficient voting power (proposalThreshold)
- Arrays must have matching lengths
- Proposal must be unique (no duplicates)
Returns:
proposalId: Unique identifier (hash of proposal data)
Example:
const governor = await ethers.getContractAt(
"ConfidentialGovernor",
GOVERNOR_ADDRESS
);
// Propose transferring funds from treasury
const targets = [TREASURY_ADDRESS];
const values = [0];
const calldatas = [
treasury.interface.encodeFunctionData("transfer", [
recipient,
encryptedAmount,
proof,
]),
];
const description = "Proposal #1: Fund ecosystem development";
const tx = await governor.propose(targets, values, calldatas, description);
const receipt = await tx.wait();
// Get proposal ID from event
const event = receipt.events.find((e) => e.event === "ProposalCreated");
const proposalId = event.args.proposalId;
Depositing Voting Power
function depositVotingPower(
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external nonReentrant returns (euint64 deposited);
Purpose: Transfer governance tokens to the governor contract to gain voting power
Process:
- Encrypt the amount to deposit
- Governor pulls tokens via
confidentialTransferFrom - Balance and available voting power updated
- Returns actual deposited amount
Example:
// 1. Encrypt deposit amount
const input = fhevm.createEncryptedInput(GOVERNOR_ADDRESS, account);
input.add64(ethers.parseUnits("100", 18)); // 100 tokens
const encrypted = await input.encrypt();
// 2. Approve governor as operator (one-time)
await governanceToken.approveOperator(GOVERNOR_ADDRESS);
// 3. Deposit tokens
const tx = await governor.depositVotingPower(
encrypted.handles[0],
encrypted.inputProof
);
await tx.wait();
console.log("Voting power deposited!");
Casting Votes
function castVote(
uint256 proposalId,
VoteType support,
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external returns (ebool);
Parameters:
proposalId: The proposal to vote onsupport: Vote direction (0=Against, 1=For, 2=Abstain)encryptedAmount: Encrypted vote weightinputProof: ZK proof for encrypted amount
Requirements:
- Proposal must be in Active state
- Voter must have available voting power
- Vote amount locked until proposal finalized
Returns:
ebool: Encrypted boolean indicating if sufficient balance
Example:
// Vote FOR with 50 tokens
const input = fhevm.createEncryptedInput(GOVERNOR_ADDRESS, account);
input.add64(ethers.parseUnits("50", 18));
const encrypted = await input.encrypt();
const tx = await governor.castVote(
proposalId,
1, // VoteType.For
encrypted.handles[0],
encrypted.inputProof
);
await tx.wait();
console.log("Vote cast successfully!");
Requesting Tally
function requestTally(uint256 proposalId) external;
Purpose: Request decryption of vote results after voting period ends
Process:
- Verify voting period has ended
- Calculate encrypted quorum check
- Calculate encrypted success condition
- Request decryption from fhEVM oracle
- Store request ID for later settlement
Oracle Callback:
function finalizeTally(
uint256 requestId,
bytes memory cleartexts,
bytes memory decryptionProof
) external;
The oracle automatically calls this with decrypted results:
quorumReached: boolsucceeded: bool
Example:
// Anyone can request tally after voting ends
await governor.requestTally(proposalId);
// Wait for oracle to call finalizeTally (automatic)
// Check final state
const state = await governor.state(proposalId);
// state will be Succeeded or Defeated
Queuing Proposals
function queue(uint256 proposalId) external;
Purpose: Schedule a successful proposal for execution in the timelock
Requirements:
- Proposal must be Succeeded
- Quorum must have been reached
- For votes > Against votes
Process:
- Verify proposal succeeded
- Schedule in timelock with minimum delay
- Mark as queued with timestamp
- Emit
ProposalQueuedevent
Example:
await governor.queue(proposalId);
const proposal = await governor._proposals(proposalId);
console.log(`Executable at: ${new Date(proposal.queueTimestamp * 1000)}`);
Executing Proposals
function execute(uint256 proposalId) external payable nonReentrant;
Purpose: Execute a queued proposal after timelock delay
Requirements:
- Proposal must be Queued
- Timelock delay must have passed
- Execution must happen within grace period
Process:
- Verify execution window is valid
- Call timelock.executeBatch()
- Timelock executes all proposal calls
- Mark proposal as Executed
- Emit
ProposalExecutedevent
Example:
// Wait for timelock delay to pass
const minDelay = await timelock.getMinDelay();
await ethers.provider.send("evm_increaseTime", [minDelay.toNumber()]);
await ethers.provider.send("evm_mine");
// Execute
await governor.execute(proposalId);
console.log("Proposal executed!");
Unlocking Votes
function unlockVotes(uint256 proposalId) external returns (ebool);
Purpose: Reclaim voting power locked in a finalized proposal
Requirements:
- Proposal must be finalized (Succeeded or Defeated)
Process:
- Retrieve locked votes for caller
- Add back to available voting power
- Clear vote usage record
- Emit
VotesUnlockedevent
Example:
// After proposal is finalized
await governor.unlockVotes(proposalId);
console.log("Voting power unlocked!");
Withdrawing Tokens
function withdrawVotingPower(
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external returns (euint64, ebool);
Purpose: Withdraw governance tokens back from governor
Requirements:
- Must have sufficient available (unlocked) voting power
Example:
const input = fhevm.createEncryptedInput(GOVERNOR_ADDRESS, account);
input.add64(ethers.parseUnits("50", 18));
const encrypted = await input.encrypt();
await governor.withdrawVotingPower(encrypted.handles[0], encrypted.inputProof);
Timelock Contract
ConfidentialTimelock
Based on OpenZeppelin's TimelockController with confidential extensions.
Key Parameters:
minDelay: Minimum time between queue and executionproposers: Addresses that can schedule operations (usually Governor)executors: Addresses that can execute operations (usually Governor)
Key Functions:
// Schedule a batch of operations
function scheduleBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads,
bytes32 predecessor,
bytes32 salt,
uint256 delay
) external;
// Execute a batch of operations
function executeBatch(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata payloads,
bytes32 predecessor,
bytes32 salt
) external payable;
// Cancel a scheduled operation
function cancel(bytes32 id) external;
Security Features:
- Time delay prevents immediate malicious execution
- Community has time to review and react
- Cancellation mechanism for emergencies
- Role-based access control
Treasury Contract
ConfidentialTreasury
Purpose: Hold and manage DAO funds with confidential accounting
Key Functions:
// Transfer confidential tokens
function transfer(
address token,
address to,
externalEuint64 encryptedAmount,
bytes calldata inputProof
) external onlyTimelock returns (euint64);
// Get encrypted balance
function getBalance(address token) external view returns (euint64);
Access Control:
- Only the timelock can authorize transfers
- Proposals must go through full governance cycle
Governance Parameters
Current Configuration (Sepolia)
const governanceConfig = {
votingDelay: 300, // 5 minutes (in seconds)
votingPeriod: 600, // 10 minutes
proposalThreshold: 100n, // 100 tokens to propose
quorumVotes: 1000n, // 1000 tokens minimum participation
executionGracePeriod: 86400, // 24 hours to execute
timelockDelay: 300, // 5 minutes minimum delay
};
Typical Mainnet Configuration
const mainnetConfig = {
votingDelay: 7200, // 2 hours
votingPeriod: 50400, // 14 days
proposalThreshold: 100000n, // 100K tokens
quorumVotes: 1000000n, // 1M tokens (10% of 10M supply)
executionGracePeriod: 259200, // 3 days
timelockDelay: 172800, // 2 days
};
Complete Governance Flow
1. Acquire Voting Power
// Mint or receive governance tokens
// Approve governor as operator
await governanceToken.approveOperator(GOVERNOR_ADDRESS);
// Deposit tokens to governor
const input = fhevm.createEncryptedInput(GOVERNOR_ADDRESS, account);
input.add64(amount);
const encrypted = await input.encrypt();
await governor.depositVotingPower(encrypted.handles[0], encrypted.inputProof);
2. Create Proposal
const targets = [TREASURY_ADDRESS];
const values = [0];
const calldatas = [
treasury.interface.encodeFunctionData("transfer", [
beneficiary,
encryptedAmount,
proof,
]),
];
await governor.propose(
targets,
values,
calldatas,
"Proposal: Fund Development"
);
3. Vote on Proposal
// Wait for voting delay to pass
await sleep(votingDelay * 1000);
// Cast vote
const voteInput = fhevm.createEncryptedInput(GOVERNOR_ADDRESS, account);
voteInput.add64(voteWeight);
const voteEncrypted = await voteInput.encrypt();
await governor.castVote(
proposalId,
1, // VoteType.For
voteEncrypted.handles[0],
voteEncrypted.inputProof
);
4. Finalize Voting
// Wait for voting period to end
await sleep(votingPeriod * 1000);
// Request tally (anyone can call)
await governor.requestTally(proposalId);
// Wait for oracle to finalize (automatic)
// Check result
const state = await governor.state(proposalId);
console.log("Proposal state:", state); // 4 = Succeeded
5. Queue & Execute
// Queue in timelock
await governor.queue(proposalId);
// Wait for timelock delay
await sleep(timelockDelay * 1000);
// Execute
await governor.execute(proposalId);
console.log("Proposal executed!");
6. Reclaim Voting Power
// Unlock votes from finalized proposal
await governor.unlockVotes(proposalId);
// Optionally withdraw tokens
const withdrawInput = fhevm.createEncryptedInput(GOVERNOR_ADDRESS, account);
withdrawInput.add64(amount);
const withdrawEncrypted = await withdrawInput.encrypt();
await governor.withdrawVotingPower(
withdrawEncrypted.handles[0],
withdrawEncrypted.inputProof
);
Events
Governor Events
event ProposalCreated(
uint256 indexed proposalId,
address indexed proposer,
address[] targets,
uint256[] values,
bytes[] calldatas,
string description
);
event VoteCast(
address indexed voter,
uint256 indexed proposalId,
VoteType support,
euint64 encryptedWeight,
ebool success
);
event ProposalFinalized(
uint256 indexed proposalId,
bool quorumReached,
bool succeeded
);
event ProposalQueued(uint256 indexed proposalId, uint256 eta);
event ProposalExecuted(uint256 indexed proposalId);
event ProposalCanceled(uint256 indexed proposalId);
event VotesDeposited(address indexed voter, euint64 amount);
event VotesWithdrawn(address indexed voter, euint64 requested, euint64 transferred, ebool success);
event VotesUnlocked(uint256 indexed proposalId, address indexed voter, euint64 amount);
Security Considerations
Vote Privacy
- All votes encrypted until final tally
- Vote weights never revealed individually
- Only aggregate results decrypted
- Prevents vote buying and coercion
Sybil Resistance
- Proposal threshold prevents spam
- Quorum requirement ensures legitimacy
- Token-weighted voting (1 token = 1 vote)
Time Delays
- Voting delay: Time to review proposals
- Voting period: Sufficient time to participate
- Timelock delay: Emergency cancellation window
- Grace period: Execution window
Emergency Procedures
- Admin can cancel malicious proposals
- Timelock can be upgraded
- Treasury has escape hatch (admin only)
Deployed Addresses
See Deployed Addresses for current contract addresses.
Related Documentation
- DAO Architecture - DAO system architecture
- Protocol Overview - Complete protocol documentation