Confidential DAO Voting Using Homomorphic Encryption

October 12, 2023
  -  
Clément Danjou

In this blog post, you will discover how homomorphic encryption can enable the creation of confidential Decentralized Autonomous Organizations (DAOs). You will get a practical guide on implementing a confidential DAO with Zama's fhEVM.

What’s a DAO and why do it confidential?

A DAO is a self-governing entity on a blockchain that leverages a governance token to enable token holders to propose and vote on decisions related to the organization. Typically, a DAO consists of two primary smart contracts: the Governor contract, which manages vote proposals and voting processes, and a Token contract that maintains a registry of token holders.

Various voting models exist, such as quorum voting or quadratic voting but regardless of the voting paradigm, the ability to cast a vote depends on token ownership, representing a stake in the project.

In a traditional DAO, voting records are publicly accessible, revealing each user's token holdings and voting choices. In contrast, a confidential DAO employs a confidential token, maintaining the secrecy of both the token quantities and individual votes.

For the purpose of this article, let’s focus on the Compound Protocol as the point of reference.

Token

In the context of this article, you will transform the COMP token contract into a confidential token contract.

The fhEVM introduces new encrypted data types like [.c-inline-code]ebool[.c-inline-code] and [.c-inline-code]euint32[.c-inline-code]. You'll proceed to update properties, including [.c-inline-code]totalSupply[.c-inline-code], [.c-inline-code]balances[.c-inline-code] and others, to use the [.c-inline-code]euint32[.c-inline-code] data type.

/// @notice Total number of tokens in circulation
    euint32 public totalSupply = TFHE.asEuint32(0);

/// @notice Allowance amounts on behalf of others
    mapping(address => mapping(address => euint32)) internal allowances;

/// @notice Official record of token balances for each account
    mapping(address => euint32) internal balances;

/// @notice A checkpoint for marking number of votes from a given block
    struct Checkpoint {
        uint32 fromBlock;
        euint32 votes;
    }

A token used within a DAO is close to a traditional ERC-20 token with some distinctions. Rather than solely monitoring a user's token balance at a specific moment, it maintains a historical record of the number of tokens held by each user over time. In the context of a proposal introduced at block N, users are eligible to vote on it using the token holdings they had at that particular block.

The token contract incorporates two methods for managing this historical data: [.c-inline-code]moveDelegates[.c-inline-code] and [.c-inline-code]writeCheckpoint[.c-inline-code].

Whenever a mint, transfer, or delegate takes place, the contract triggers the [.c-inline-code]moveDelegates[.c-inline-code] function to create a checkpoint reflecting the user's token holdings at the current block number.

Transitioning this function to use encrypted integers is relatively straightforward, as illustrated. The primary distinction lies in replacing the default [.c-inline-code]0[.c-inline-code] with [.c-inline-code]TFHE.asEuint(0)[.c-inline-code] to obtain an encryption of zero.

function _moveDelegates(address srcRep, address dstRep, euint32 amount) internal {
        if (srcRep != dstRep) {
            if (srcRep != address(0)) {
                uint32 srcRepNum = numCheckpoints[srcRep];
                euint32 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : TFHE.asEuint32(0);
                euint32 srcRepNew = srcRepOld - amount;
                _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew);
            }

            if (dstRep != address(0)) {
                uint32 dstRepNum = numCheckpoints[dstRep];
                euint32 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : TFHE.asEuint32(0);
                euint32 dstRepNew = dstRepOld + amount;
                _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew);
            }
        }
    }

Apart from the function signature, there's no need to make any changes to the [.c-inline-code]writeCheckpoint[.c-inline-code] function: [.c-inline-code]writeCheckpoint[.c-inline-code] will store the encrypted number of votes for a specific block.

Aside from changes in methods like [.c-inline-code]balanceOf[.c-inline-code] or [.c-inline-code]transfer[.c-inline-code] that were already discussed in the ERC-20 article and changes in function signatures, the token contract is good to go.

Governor

The Governor contract is responsible for managing proposals and votes. When the token amount is encrypted, you’ll want to keep the vote choice secret as well. To ensure vote confidentiality, we'll make changes to the ciphertext for both the total "for" and "against" votes.

The fhEVM provides a method which acts as a ternary operator on encrypted integers. This method is called cmux. When using [.c-inline-code]TFHE.cmux[.c-inline-code] to assign a value, the result changes, even if the plaintext value remains unchanged. This is useful because each time a vote is cast, both ciphertexts will be updated on the chain.

function _castVote(address voter, uint proposalId, ebool support) internal {
        require(state(proposalId) == ProposalState.Active, "GovernorAlpha::_castVote: voting is closed");
        Proposal storage proposal = proposals[proposalId];
        Receipt storage receipt = proposal.receipts[voter];
        require(receipt.hasVoted == false, "GovernorAlpha::_castVote: voter already voted");
        euint32 votes = comp.getPriorVotes(voter, proposal.startBlock);
        
        proposal.forVotes = TFHE.cmux(support, proposal.forVotes + votes, proposal.forVotes);
        proposal.againstVotes = TFHE.cmux(support, proposal.againstVotes, proposal.againstVotes + votes);

        receipt.hasVoted = true;
        receipt.votes = votes;

        // `support` and `votes` are encrypted values, no need to include them in the event.
        // emit VoteCast(voter, proposalId, support, votes);
        emit VoteCast(voter, proposalId);
    }

In our confidential Governor version, there exists a proposal threshold. Essentially, individuals with fewer than a specific number of tokens are ineligible to propose or cancel a vote. Since the amount of votes is encrypted, you must make adjustments to this verification process.

require(TFHE.decrypt(
    TFHE.lt(proposalThreshold(), comp.getPriorVotes(msg.sender, block.number - 1))
  ),
  "GovernorAlpha::propose: proposer votes below proposal threshold"
);

We are doing a homomorphic comparison between the proposal threshold and the number of votes using [.c-inline-code]TFHE.lt[.c-inline-code] which returns an encrypted boolean value. Once decrypted, you can use it with [.c-inline-code]require()[.c-inline-code] function to revert the transaction if needed.

Conclusion

Transitioning a DAO contract from an unencrypted state to an encrypted one has proven to be a straightforward process. fhEVM offers a user-friendly solution for writing smart contracts with encrypted data and performing homomorphic computations.

You can find Governor contract, Comp contract and more examples on the fhEVM repository. If you want to try fhEVM and write your own contract, we have a documentation, community support and even a bounty program.

Additional links

Read more related posts

Onchain Blind Auctions Using Homomorphic Encryption and the fhEVM

Blind auctions are notoriously hard to do in smart contracts, since all the transaction and states are public... But not anymore.

Read Article

Confidential ERC-20 Tokens Using Homomorphic Encryption and the fhEVM

FHE can be used in blockchain to enable confidential smart contracts, where transaction inputs and states remain encrypted.

Read Article

Private Smart Contracts Using Homomorphic Encryption

Rand Hindi's talk at EDCON 2023 about "Private Smart Contracts using Homomorphic Encryption".

Read Article