Do we need to delegate votes *before* a proposal is active?

Hi there, I'm writing tests for governance functions that are testing the functionality of contracts created with the wizard (https://wizard.openzeppelin.com/#governor).

One thing that didn't seem super intuitive was delegating votes. The first issue was wondering why my voters ERC20 tokens didn't count at first. It terms out they needed to self-delegate:

Once a proposal is active, delegates can cast their vote. Note that it is delegates who carry voting power: if a token holder wants to participate, they can set a trusted representative as their delegate, or they can become a delegate themselves by self-delegating their voting power.

So that is solved, but I noticed another issue. It seems that if a proposal already exists, a user can not self-delegate and vote on that proposal afterward and have the votes be expected to count. Is that expected functionality, or is that a bug?

If it is expected, could we get an explanation as to why post proposal creation delegate is not allowed? Combined with the fairly unexpected reason that a user would have to self-delegate, I think a lot of people would be surprised if they can't vote on an active proposal if they have not self-delegated previously.

Thanks in advance!

Not sure if there is a "Governance" tag or not to help make this more findable but hopefully this is right spot.

Yes, you need to bro

Why can't they be done after the proposal, it seems way more intuitive.

Say Alice hasn't participated in governance so far, and has never delegated any tokens. But she comes across a new proposal that catches her attention.

She gets excited and votes on the proposal, only to find out here tokens are worthless for this proposal because they weren't delegate previously.

This seems like a terrible user experience for new voters looking to actually get involved with governance, and this unexpected disappointment will be off-putting to them.

We want to avoid someone beeing able to vote twice with the same tokens:

  • Alice has 10 tokens
  • Alice votes
  • Alice send the 10 tokens to Bob,
  • Bob votes
    → the 10 token have produced 20 votes, this is bad.

The solution for that is to use snapshots. You vote with the tokens you had at the moment of the snapshot.... more precisely, you vote with the tokens delegated to you at the moment of the snapshot.

In the governor, the snapshot is when the vote start. It can be at the moment when the proposal is made, or it can be latter (see the vote delay in the documentation).

In other to vote, the delegation must happen BEFORE the snapshot.

1 Like

I'm sorry for Alice if the is disappointed, but that is essential to enshure the vote is fair. Hopefully, the governor frontend will let her know that she must delegate (to herself?).

Wouldn't taking the snapshot at execution time make the most sense? It seems like that's when votes are actually counted to determine if quorum is reached and if FOR votes > AGAINST votes.

If is created at execution time,, there would not be a time frame for anyone to back out if they are no longer interested in the proposal

... when does that ever happen? Who votes and then is later like "oh please no don't count it anymore!"?

And surely the people who have that problem are far fewer than the people who would think this pre-delegate proposal behavior is not intuitive.

Just there incase their is a need

It's a good question but votes are not counted at that time. Because of limitations of the blockchain, votes are counted incrementally as they are cast. We can't count them all at the same time. So the snapshot needs to be taken before it is possible to cast votes. This is the function of the votingDelay parameter.

1 Like

Interesting! From the docs, votingDelay seemed to come off as just some arbitrary delay for voting with no real purpose. If this is indeed a period where a proposal has been announced, but users can still delegate themselves, then that makes a lot more sense!

@fragio I have one other question: why isn't self-delegate functionality automatic?

It seems like the unintuitive behavior (alice can't vote on a proposal because she didn't delegate herself beforehand) can be avoided if, whenever her token balance went above 0, she was already self-delegated.

Couldn't this be accomplished cheaply by doing this in _afterTokenTransfer:

// If person receiving tokens has not delegated to anybody and has 0 tokens so far, self-delegate.
if (delegates(to) == address(0) && balanceOf(to) == 0) {
   _delegate(to, to);
}

Delegation is opt-in because it implies additional cost on transfers. If a token holder is only interested in trading and not governance, then delegation is a cost they don't need to pay.

Let me try to grasp what the definite challenge here is. I think this is what is being referred to: _afterTokenTransfer does _moveVotingPower(delegates(from), delegates(to), amount).

function _moveVotingPower(
        address src,
        address dst,
        uint256 amount
    ) private {
        if (src != dst && amount > 0) {
            // block 1
            if (src != address(0)) {
                (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(ERC20VotesStorage.layout()._checkpoints[src], _subtract, amount);
                emit DelegateVotesChanged(src, oldWeight, newWeight);
            }
            // block 2
            if (dst != address(0)) {
                (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(ERC20VotesStorage.layout()._checkpoints[dst], _add, amount);
                emit DelegateVotesChanged(dst, oldWeight, newWeight);
            }
        }
    }
  1. If neither the sender nor receiver has delegated, both block 1 and block 2 will be skipped
  2. If the sender has delegated but not receiver, block 1 will only be hit.
  3. If the receiver has delegated but not sender, block 2 will only be hit
  4. If both have delegated, both blocks will be hit.

But please correct me if my understanding is wrong or if there are other factors driving up potential gas cost of delegated.

I'm not sure what the question in this last post is. But your understanding seems correct.