|

How OpenZeppelin Governor works | Smart Contract Review Series

Doing Smart Contract reviews of the popular protocols that have battle-tested smart contracts is the best way to learn. Courses or tutorials rarely teach us how to write production-ready code, so looking at something that is live on Mainnet and hasn’t been hacked (yet), is very useful. (Actually, it’s equally important to study smart contracts that have been hacked, more on that later).

Code reviews are not just for learning but for stealing great ideas from the code. The Blockchain industry is where most of the code is reused, and that is very smart because the bugs can have serious consequences here, and you have to get it mostly right the first time around; you can’t deploy bugfixes regularly.

Intro to Governor Smart Contract

I’m starting this Code Review Series by reviewing the Open Zeppelin Governor contract. The Governor is an essential Smart Contract required for managing voting within a DAO.
Its main job is to create proposals, allow voting on them and then execute proposals if they have been voted positively.

They already have excellent documentation, but what I’m going to do is review line by line the most important parts of the code and deconstruct every piece of logic.

The complete source code is on Github. In this article, I’ll copy it over piece by piece.

Reviewing storage variables

Let’s start at the beginning and look at what’s being stored.

abstract contract Governor is Context, ERC165, EIP712, IGovernor {
    using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
    using SafeCast for uint256;
    using Timers for Timers.BlockNumber;

    bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)");
    bytes32 public constant EXTENDED_BALLOT_TYPEHASH =
        keccak256("ExtendedBallot(uint256 proposalId,uint8 support,string reason,bytes params)");

    struct ProposalCore {
        Timers.BlockNumber voteStart;
        Timers.BlockNumber voteEnd;
        bool executed;
        bool canceled;
    }

    string private _name;

    mapping(uint256 => ProposalCore) private _proposals;

    DoubleEndedQueue.Bytes32Deque private _governanceCall;

Open Zeppelin Governor inherits from multiple contracts:

  1. Context: this is for getting msg.sender and msg.value in a safe way.
  2. ERC165: this is a standard that adds functionality for inspecting if a contract implements an interface.
  3. EIP712: is a standard for hashing and signing of typed structured data. It helps to allow users to cast votes by signing messages.
  4. IGovernor: simply an interface that defines the required Governor functions.

BALLOT_TYPEHASH and EXTENDED_BALLOT_TYPEHASH are simply the hashes used for casting votes by signing messages; we’ll see this later.

ProposalCore is the struct that defines proposals, obviously. It has block start and end times and two flags: executed and canceled; they’re pretty self explanatory.

In the end, we have _name, which is the name of the Contract.

_proposals: mapping that stores the proposals made by their creator.

_governanceCall is a double-ended queue (queue that can be read from both sides), that is used when the Governor is performing operations on itself.

Working with proposals

The main reason we would use a Governor contract is to be able to create proposals, vote on proposals and execute proposals. To execute a proposal is to simply run a Smart Contract function if the votes pass, this function usually sends tokens to some third party that would do some work for the DAO, fund some non-profit organization, or maybe just modify part of a protocol with a Smart Contract.

Hash Proposal

function hashProposal(
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        bytes32 descriptionHash
    ) public pure virtual override returns (uint256) {
        return uint256(keccak256(abi.encode(targets, values, calldatas, descriptionHash)));
    }

This is a very simple function that creates a unique hash for each proposal based on targets, values, calldatas and descriptionHash. I’m starting with this one because it illustrates what a proposal actually is.

A proposal contains an array of targets, that are addresses: Smart Contracts or EOAs.

An array of values, representing how much value/ ETH will be sent to the target addresses.

Another array of calldatas, represents which methods will be called on the target addresses.

These three arrays allow us to execute any function on any smart contract, or send any token to any address, it allow for arbitrary logic to be executed on the blockchain.

Finally, we have a hash of a description that explains the reason for the proposal in the first place.

With these

Creating a proposal

Now that we know what a proposal is, here’s how it gets created. We have the same input parameters as above, but there’s a lot more logic here, let’s start at the beginning.

Validation

A few validation checks are required to allow the creation of a new proposal. First, the person proposing must have enough voting power to create a proposal, this requirement is set depending on the project.

Then, we make sure the arrays of targets, values and calldatas all have the same length and they’re not empty.

Next, we make sure the proposal doesn’t already exist. This is checked by inspecting if proposal.voteStart is unset. Mappings always return default values so we need to be carefull.

Snapshot and Deadline

The last part is to take care of the snapshot and deadline:

uint64 snapshot = block.number.toUint64() + votingDelay().toUint64();
uint64 deadline = snapshot + votingPeriod().toUint64();

proposal.voteStart.setDeadline(snapshot);
proposal.voteEnd.setDeadline(deadline);

Snapshot is the block number at which the voting can start but also the block number at which the voting weights will be computed from. Each voter vote does not count as one, its weight is proportional to how many governance token the voter has. And we check how many governance tokens the voter has at the block of the snapshot.

Deadline is another block number that is quite self-explanatory. It tells us what is the deadline for voting.

In the end we just emit a ProposalCreated event that contains everything necessary regarding the proposal.

What’s interesting here is that we are not storing the targets or the values or the calldatas for the proposal in the smart contract.

So how will we know what logic to execute on the successful proposal?

Well, the hash is enough to verify that the proposals we want to execute have the same inputs as when we created them.
But in order to get the inputs at this later stage, we need to manually get them from the ProposalCreated event, this is done off-chain, but still, the verification is on-chain so it’s all good.

Execute proposal

This is the public execute function that does some validation and then calls the internal _execute function that actually runs the proposal.

The important thing to check here is the proposal status, we check that it must be succeeded or queued.

Next, we mark the proposal as executed, following the Checks-Effects-Interractions pattern.

The final part is the most interesting. So far anyone could’ve written the Governor contract logic, it’s not a big deal. But taking care of the edge cases is what’s important in Smart Contract development.

What I’m talking about is the _beforeExecute <–> _execute <–> _afterExecute sandwich logic in the end.

Before Execute

Let’s look at _beforeExecute first:

In a lot of DAOs, the executor is not the Governor token itself but a timelock contract. Timelock Contracts make sure there is a delay between the proposal’s success and execution.

That’s the reason for the first if-condition.

Next, we go over all targets and find out where the address is equal to the Governor’s contract itself. We push the hash of the calldata, to the _governanceCall queue.

This is used in the onlyGovernance modifier that you see below.

It removes the elements of the queue until it finds a hash that matches the hash of _msgData or the current function being called. This modifier enforces to a function that it must be in the queue if it is to be executed. And for it to be in the queue, it must be put there as part of the execute mechanism.

This prevents Governor functions from being executed outside of a proposal, because the Governor is often times the holder of the treasury.

Execute

The internal execute function is where the magic happens, the actual functions are called on the proposal targets with the provided values. But it’s still simple, it’s just a loop through those arrays. I’ll paste it again for completeness here.

After execute

The after execute function is the simplest, it just clears out the onlyGovernance mapping.

Voting on proposals

So far we saw how proposals are created and executed, now let’s look at the final piece of the puzzle, voting.

Validation at the beginning as usual, in this case we require the proposal status to be “active”. The proposal is active when it’s in the period where voting has been started but it’s not finished yet.

Then we call _getVotes, to get the vote weight of the voter and _countVote to actually count the vote towards the proposal. However, both of these functions are virtual because they can be implemented in many different ways. One difference is that votes can be represented by either ERC20 token or ERC721 (NFT) token.

VoteCast or VoteCastWithParams event is emitted in the end and that’s all.

GetVotes implementation

Let’s look at a simple implementation of the getVotes virtual function from above. The responsibility of this function is to retrieve the weight of the user’s votes based on their address and a block number.

One simple implementation can be found under the extensions folder in the OpenZeppelin repository and that is GovernorVotes.sol

Usually when voting in DAOs, it’s common to consider the voting weight at some point in the past instead of right now. To implement this, a special type of token is used here IERC5805, that has the function token.getPastVotes(account, timepoint).

This function does exactly what it says, it reads how many tokens the account had at a certain timestamp in the past. Basically, a record is created any time a transfer of these tokens occurs, that’s how it knows.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *