Fractional Governance Voting
See original GitHub issueđ§ Motivation
Weâd like to implement a version/extension of Governor that enables delegates to distribute their total voting weight fractionally across For/Against/Abstain. There are many possible uses for this, many of them related to allowing owners of governance tokens which are pooled to participate in Governance votes. Take, for example, cUNI community voting.
đ Details
In current Governor implementations, a delegate assigns all their voting weight to a single choice: For/Against/Abstain. Specifically, this is implemented concretely in the _countVote
implementation in GovernorCountingSimple.
Weâd like to implement a version that enables the delegate to split their voting weight fractionally. For example, a delegate with a weight of 1,000 might assign 700 to For, 100 to Against, and 200 to Abstain.
One challenge with this idea is that it canât trivially be implemented as an extension to the existing Governor implementation or interface in the same way as GovernorCountingSimple.
Specifically, GovernorCountingSimple implements the virtual _countVotes
method, which is itself called by the internals of the concrete Governor.
A hypothetical GovernorCountingFractional
faces a challenge here, because there is no where in the signature of _countVotes
method to include data about how to fractionalize the votes. The only available parameter is the support
parameter, but as a uint8
, itâs not big enough to hold data about how to split votes between 3 discreet options.
Given this, weâd like some feedback on how we might go about implementing this feature in a way youâd be likely to accept. There are any number of options, but here are a few to consider:
- Change the signature of
_countVote
, and thecastVote
family of methods, to makesupport
a uint256. With the extra bits, we could pack the vote counts/proportions for each options into thesupport
parameter. This is an easy approach, with minimal surface are for the implementation, but the obvious downside is backwards compatibility as it changes the signatures. - Extend Governor and add a new
castWeightedVote
family of methods. This works, and contains the fractional voting changes to a single new file, but it feels a bit hacky. ThecastWeightedVote
methods added would exist on the inheriting class, but effectively âredoâ much of the functionality from the baseGovernor
. It eschews the benefits of using the inheritance pattern in the first place. - Create a new IGovernorFractional interface and GovernorFractional implementation. In this version, the IGovernorFractional would extend and add fractional voting methods to the IGovernor interface. The GovernorFractional interface would implement the
castWeightedVote
methods. Finally, aGovernorFractionalCountingSimple
implementation would provide a concrete implementation of a new, virtual_countFractionalVote
method. Internally, the_countVote
implementation could curry to the new_countFractionalVote
method for backwards compatibility with the inherited, non-fractional voting methods.
Weâre totally open to feedback on this feature proposal, along with the best way to go about implement it. We believe this feature could be beneficial to many projects opting for OZ implementations of Governance. Weâre eager and willing to tackle the implementation ourselves and open a PR, but want to do it in a way that will fit with the rest of the library. Thanks in advance for your consideration!
Issue Analytics
- State:
- Created 2 years ago
- Comments:11 (11 by maintainers)
Hey @frangio, thanks so much for the thoughtful response and proposal! I definitely agree in principle that if we could find a way to make this work without changing the interface, that would be ideal. I do see some potential issues with your proposal, though, so let me share some of those to get your thoughts.
First, the precision issue. With 255 steps, the maximum precision we can represent is about 0.4%. This might be an acceptable tradeoff, but itâs not insignificant, especially given one of the top imagined use cases for fractional voting is large pools of governance tokens held in contracts. Take, as a representative example, the cUNI pool, which has something on the order of 10 Million UNI. If that pool were to leverage this method to allow cUNI holders to express their preferences, it could result in an error in weighting of 40,000 UNI. Thatâs currently >$1 Million in economic weight, and more than enough to swing the result in the case of a modestly contentious vote.
The second issue is related to representing abstentions. If Iâm understanding your proposal correctly youâre arguing that splitting âabstainâ votes equally between For and Against would have the same effect as Abstain. However, this doesnât seem to be the case, at least not with regards to the way abstentions are implemented by Bravo (and your concrete implementations which follows Bravo).
In short, in the current system:
This makes Abstain a discreet option, one which has its own impact on the results of the vote. These impacts are different from splitting the same number of votes between For and Against. Changing the definition of Abstain to mean âsplit between For and Againstâ might be a reasonable choice, but it would be a meaningful one that a DAO would have to consider carefully. It certainly would impact the game theoretic ways in which this feature could be used.
One additional complication here: âNo Voteâ, i.e. not voting at all is, in a way, a discreet option for voting as well. Itâs a debatable question whether a fractional voting system should enable a delegate (which, itâs important to remember, might be a contract itself) to vote with only a fraction of their weight, or whether it should require all weight be split between For/Against/Abstain. One could imagine a pool with 1000 weight choosing only to vote with 600 of its voting power 300 For/100 Against/200 Abstain, leaving 400 as âNo Voteâ. Whether this âshouldâ be implemented or not, it would be nice if the architecture at least made it possible for a project to do so.
With all the above in mind, if youâll humor me, I want to take a minute to âlobbyâ for biting the bullet and making the small breaking change to upgrade the
support
parameter fromuint8
touint256
.Regardless of how we ultimately tackle fractional voting, I think undertaking this exploration has revealed something to me: the current interface is pretty limited in its ability to be extended to implement alternative voting schemes. We can find a way to make fractional voting work, but the underlying limitation may still prove restricting to other experiments in the future.
Let me give a completely contrived example. Imagine some project wanted to enable voting where delegates could express not only their preference For/Against/Abstain, but also an amount. This amount might represent the quantity of governance tokens to be awarded as a grant, and the vote counting implementation might do something like calculate a voting-power-weighted average to determine the final number. So if the proposal was to grant tokens to a community fund, one delegate might vote Against, while another might vote For/10,000, and another might vote For/30,000.
Implementing the above example (which, again, is completely contrived and just meant to be representative of a multitude of possible voting schemes) would be impossible using the current interface. A would-be implementor would have to do something similar to what Iâm proposing in Option 2 or Option 3 in the original issue above, except for their specific usecase, instead of for Fractional Voting.
On the other hand, if the
support
parameter wasuint256
, they could easily pack both the amount and the preference in the 256 bits available. Their implementation would simply be an extension of Governor with a custom_countVotes
method to unpack the bits, and do whatever calculations/storage are necessary.In other words, the architecture youâve chosen hereâ with the virtual
_countVotes
method that can be overriden and implemented concretely to count votes in any custom wayâ is a really really nice abstraction, and makes creating custom implementations really clean and easy to reason about. But, itâs currently hamstrung by a lack of flexibilityâ thereâs just not enough surface area to include more data.Bumping the
support
param touint256
is a small change that gives the system significantly more flexibility. Given this has not yet been codified as a standard, or deployed too widely by too many projects, it feels worthwhile to seriously consider making the breaking change.Ok, thatâs my pitch đ . Very curious to hear what you think. Thanks again for your time and thoughtful consideration here!
Yes this is a valid question. My argument in favor of the generic data parameter is that I think we need it supported at the lower level in the core Governor contract, and passed internally to the relevant functions, if we want to build customizations on top of it like the ones weâve mentioned in this thread.
The external interface can offer functions with specific parameters for fractional voting or other features, but would be implemented by this generic primitive internally.
This can all be solved perhaps with less work by forking the code and making the ad-hoc changes required, but having this generic primitive internally is a building block that can be reused for some of the other use cases that are currently not covered.
The encoding problem is not different from the one already present for the
uint8 support
parameter. An integrator needs to know how to encode a âForâ vote into a uint8 value, for example. The way that weâve tried to solve this is by having the contract self-document the encoding it expects, through theCOUNTING_MODE
getter:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/e63b09c9ad3a45484b6dc304e0e99640a9dc3036/contracts/governance/extensions/GovernorCountingSimple.sol#L36-L37
Eventually there would be a registry that documents what a particular value for
support
means. For example,support=bravo
means that Against is encoded as 0, For encoded as 1, and Abstain encoded as 2.If the generic data parameter were directly exposed (as opposed to wrapping the internal mechanism in an ad-hoc external function), what I had in mind was that the contract can similarly document the encoding in
COUNTING_MODE
, maybe under another key.