Voting is a Safe module which is a thin wrapper over OpenZeppelin’s Governor. When installed in a Safe, it will be able to execute transactions through the Safe when approved by a token vote.

For Firm it’s been built to work together with Captable and allow shareholders to control some important aspects of the main Safe such as changing its owners (as a way of electing members of the board of directors.)

Technical implementation

We haven’t implemented any custom logic for Voting which is not in the base OpenZeppelin contracts. The exact configuration chosen for Firm’s Voting in terms of OZ Governor components can be seen in OZGovernor.sol.

In summary, the chosen components were:

  • Governor Votes (link to OZ docs): as the voting power sourcing component. It checks for voting power and total number of votes on a ERC20Votes token. We had to slightly modify it for Firm with GovernorCaptableVotes in order to make it comply with our reduced ICaptableVotes interface and make storage compatible with upgradeability.

  • Governor Counting Simple (link to OZ docs): as the counting mechanism. It allows for ballots with three options: For, Against and Abstain. It determines that a proposal has been successful if the number of For votes is higher than Against votes and it has passed its quorum threshold if the sum of all For and Abstain votes is greater than the quorum requirement.

  • Governor Votes Quorum Fraction (link to OZ docs): as the quorum calculator component. It defines quorum as a fraction of all the votes that could potentially be casted. We had to slightly modify it to make it compatible with GovernorCaptableVotes. We use 10,000 as the base for the quorum numerator (e.g. 2,000 = 20% quorum requirement).

  • Governor Settings (link to OZ docs): to allow Voting parameters to be upgradeable using proposals. Note: even though the Safe is the executor for Voting proposals, making settings updates always requires a proposal passing (i.e. the Safe can’t just trigger a change)

The main notable difference from the standard OZ components is that when executing the actions associated to a proposal which has passed, rather than performing the calls directly from the Governor (or another executor like a Timelock), these calls are executed directly from the Safe’s context (using the same technique as we do for Budget’s multipayment execution).

Voting settings

The following settings are passed to Voting on its initialization:

  • Quorum numerator: fraction of the total voting power that must cast a For or Abstain vote for the vote to meet quorum.

  • Voting delay: Delay (in number of blocks) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes.

  • Voting period: Delay (in number of blocks) since voting in the proposal starts until voting ends.

  • Proposal threshold: The number of votes required in order for a voter to submit a proposal.

Note that all time periods are expressed in numbers of blocks. This allows precise snapshotting of token balances. On Ethereum mainnet, after the POS transition, the amount of time between blocks is constant and predictable at 12 seconds. This will vary depending on the block proposal and consensus mechanism of network used.

As explained in the section above, all these settings can only be changed by a vote. Trying to perform a change through the Safe directly without a successful vote will fail.


Creating a proposal

In order to create a proposal, the following function (from Governor) is used:

function propose(
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    string memory description
) public virtual override returns (uint256) {

It requires the proposer to have enough voting power to meet the proposal threshold.

Casting votes

Votes can be cast by calling Voting:castVote(uint256 proposalId, uint8 support) or Voting:castVoteWithReason(uint256 proposalId, uint8 support, string reason).

The support parameter defines the vote being cast:

  • Against: 0

  • For: 1

  • Abstain: 2

Proposal execution

After a proposal’s voting period is over, the proposal can be executed if it was successful. A proposal is considered successful if its quorum requirement was met (only For and Abstain votes are counted for this) and there were more For than Against votes.

The proposal can then be executed using the following function in which the original proposal parameters must be passed:

function execute(
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    bytes32 descriptionHash

When a proposal is executed, given that Voting is a module in the Safe, it will execute the proposal actions in the context of the Safe (via a module delegatecall)

Changing parameters

As explained above, only a Voting proposal can change the four Voting parameters.

  • Changing quorum numerator: voting proposal must call Voting:updateQuorumNumerator(uint256 newQuorumNumerator). The new numerator applies for all new proposals and also proposals which are currently pending (in the middle of their voting delay.)

  • Changing voting delay: voting proposal must call Voting:setVotingDelay(uint256 *newVotingDelay). The new voting delay only affects new proposals. If there are any pending proposals, those will still have the previous voting delay.

  • Changing voting period: voting proposal must call Voting:setVotingPeriod(uint256 newVotingPeriod). The new voting period only affects new proposals. If there are any pending or active proposals, those will still have the previous voting period.

  • Changing proposal threshold: voting proposal must call Voting:setProposalThreshold(uint256 newProposalThreshold). The new threshold applies to new proposals only. If a pending or active proposal was created by an account that no longer meets the threshold after the change, the proposal won’t suffer any changes.

Last updated