Skip to main content

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:

  1. Token Holders deposit ConfidentialGovernanceToken into the Governor
  2. ConfidentialGovernor manages proposal lifecycle and encrypted voting
  3. ConfidentialTimelock enforces delays and executes approved proposals
  4. 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 call
  • values: Array of ETH values to send (usually 0)
  • calldatas: Array of encoded function calls
  • description: 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:

  1. Encrypt the amount to deposit
  2. Governor pulls tokens via confidentialTransferFrom
  3. Balance and available voting power updated
  4. 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 on
  • support: Vote direction (0=Against, 1=For, 2=Abstain)
  • encryptedAmount: Encrypted vote weight
  • inputProof: 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:

  1. Verify voting period has ended
  2. Calculate encrypted quorum check
  3. Calculate encrypted success condition
  4. Request decryption from fhEVM oracle
  5. 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: bool
  • succeeded: 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:

  1. Verify proposal succeeded
  2. Schedule in timelock with minimum delay
  3. Mark as queued with timestamp
  4. Emit ProposalQueued event

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:

  1. Verify execution window is valid
  2. Call timelock.executeBatch()
  3. Timelock executes all proposal calls
  4. Mark proposal as Executed
  5. Emit ProposalExecuted event

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:

  1. Retrieve locked votes for caller
  2. Add back to available voting power
  3. Clear vote usage record
  4. Emit VotesUnlocked event

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 execution
  • proposers: 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.

Contract Sources