Self-delegation in ERC20Votes

Hi!

I'm using the ERC20Votes extension for my ERC20 token and I would like to assign balance = voting power.

If I understood the documentation correctly, in order to have your voting power = token balance, each wallet should self delegate itself.

By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. Enabling self-delegation can easily be done by overriding the delegates function. Keep in mind however that this will significantly increase the base gas cost of transfers. (source)

Should this happen only once in order to activate checkpoints?

It's still not really clear on how this should work.
If someone has an example of a Token implementation that has balance=voting power, that would be awesome!

Thanks for your time!

Yeah, if you want voting power = token balance, you should delegate to yourself.
And if you only want to delegate to yourself, yes, you just need to do this once.

1 Like

The token will have multiple holders and lots of transfers.
I still don't understand if I should self-delegate to each new holder or only once to the owner of the token.

Do you have any example of an implementation of the self-delegate part?

When a user receives the ERC20 token that has the ERC20Votes extension (like buying from a CEX or trading for them in a DEX), they simply have to submit a transaction to the token contract to the function delegate(address delegatee) (similar concept to calling the approve function). The user will put their wallet address in for the delegatee argument to self delegate. Then, as long as the user holds their ERC20 tokens, they will retain the votes they delegate to themselves through this delegate transaction.

2 Likes

If you want to automatically trigger self-delegation for everyone, you could override _mint or _beforeTokenTransfer to add a call to _delegate.

1 Like

I was wondering what is the better use case than token balance to have it as voting power ?

Why wouldn't I want to call delegate for each user so that their accounts can be in the checkpoints and then use my voting contract to account for this ?

I guess, the only use case I can think of is that if users that don't participate in the voting, they don't need to call delegate and that's it, right ? @frangio

Checkpoints are expensive book-keeping. It wouldn't make sense for a Uniswap Pair contract to track its balance checkpoints, for example, or an arbitrage bot. So delegation is opt-in as an optimization.

Hi @frangio ,

Would not it be the _afterTokenTransfer that needs overriding? I would think that when _beforeTokenTransfer is executed, the user does not yet have the tokens so how can they delegate the tokens to themselves. Whereas when _afterTokenTransfer is executed, the user would have the tokens. Just want to make sure I understand the sequence.

Thanks!

Good question. It's indifferent, but it may be a bit cheaper to do it in _afterTokenTransfer. The reason it's the same is that once an account defines a delegate, all of its balance is delegated including what it is about to receive.

1 Like

Thanks for the reply and explanation @frangio . Much appreciated.

Hi @frangio,

You mention to override afterTokenTransfer or _mint and include _delegate there. Let's say in every mint, I included _delegate as well. In such case, if user mints 2nd time, _delegate will still be called for no reason. Isn't it like a bad thing?

The comments in the doc states to override {delegates} function.

What if I do the override for this:

function delegates(address account) public view virtual returns (address) {
        return _delegates[account];
    }

and instead of this, I got

function delegates(address account) public view virtual returns (address) {
        return account;
    }

What downside will it have ? it seems to work.

Sorry, this was an inaccurate comment that we removed a few weeks ago but the site wasn't updated yet.

Overriding delegates will not work correctly. For example, an account will still be able to change their delegate to another account but delegates will continue to return the same account and this will lead to inconsistencies.

The correct solution as far as I can tell is to use the transfer hooks. The implementation that I described above is perhaps too simplistic. We'd have to refine what behavior exactly we want. For example perhaps only for mints where the receiving account doesn't have a delegate already.

Hi @frangio Thanks for the comments.

Maybe you can give it a look and see if my solution is now good.

I included explanation in comments below. WDYT ?

contract GovernanceWrappedERC20 is ERC20VotesUpgradeable, ERC20WrapperUpgradeable {
   // The functions below are overrides required by Solidity.
    function _afterTokenTransfer(address from, address to, uint256 amount) internal override(ERC20VotesUpgradeable, ERC20Upgradeable) {
        super._afterTokenTransfer(from, to, amount);
        // This means that minting happens. and this will only be called 
        // at those times that minting happens only. For transfers, it doesn't
        // make sense to be calling `_delegate` again and again, since more gas
        // costs for no reason. Though, I don't like the below as well, because
       // if user mints 5 times, calling _delegate 5 times is not good. All. 
      // I am saying is calling _delegate should only be needed only once 
      //  and once in lifetime.First of all, is below correct and if there's a better way 
      // such as calling `_delegate` multiple times is not needed.
        if(from == address(0) && to != address(0)) {
            _delegate(to, to);
        }
    }

}

Thank you.

This looks ok, but has the downside that if an account has already customized their delegate, a mint would overwrite it. So you can do something like:

    function _afterTokenTransfer(address from, address to, uint256 amount) internal override(ERC20VotesUpgradeable, ERC20Upgradeable) {
        super._afterTokenTransfer(from, to, amount);
        if(from == address(0) && to != address(0) && delegates(to) == address(0)) {
            _delegate(to, to);
        }
    }
1 Like

hi @frangio

I'd appreciate your opinion on this as it's pretty tricky.

After some time, I figured the following check wouldn't be good.

function _afterTokenTransfer(address from, address to, uint256 amount) internal override(ERC20VotesUpgradeable, ERC20Upgradeable) {
        super._afterTokenTransfer(from, to, amount);
        if(from == address(0) && to != address(0) && delegates(to) == address(0)) {
            _delegate(to, to);
        }
    }

The reason is _delegate would only execute in the minting scenario. What if the following scenario happens.

// 1. minted 10 to addr1 at block 230
// 2. addr1 transfers 5 to addr2
// 3. addr1 creates a proposal which for the voting, uses `getPastVotes(account, proposalCreatedBlockNumber)`
// 4. addr1 can vote, but addr2 can NOT vote even if addr2 calls delegate.

In order to solve this, First I thought to do the following:

if(to != address(0) && delegates(to) == address(0)) {
      _delegate(to, to);
}

but the problem here is the above scenario I mentioned would work, but if user manually turned of delegation(called delegate(address(0)), and someone transfered tokens to him, the code would turn on the delegation again, which is bad(we're breaking his rights in some sense). It seems to me the best case is, by default, I turn on delegation for every user(even in mint case or transfer case), but if the user manually turned it off, it's his choice so we shouldn't turn it on again in another transfer. So I come up with this:

// Will only be true in mint/transfer case and if we never turned delegation for the user before.
if (to != address(0) && numCheckpoints(to) == 0) {
    _delegate(to, to);
 }

NOTE that doing the same check as above for from is pointless. because from at some point in time would be to and it would already have delegation turned on. and if user turned it off, we shouldn't turn it on again.

Can you think of any implications ?

There are still edge cases in your example. If the account delegates prior to having received tokens you don't want to overwrite their delegate, and they will not have any checkpoints if they've delegated to someone else.

I think your best bet is to add a new variable to store whether the user has manually opted in to any form of delegation (mapping (address => bool)), and only if they haven't you set the default delegate for them.

@frangio

Aren't there really anything in ERC20Votes that could solve this ?

So, you suggest that I override delegate function in which I do:

userDelegations[account] = true;
super._delegate(...);

I'm trying to not bring new state as for the first time, the gas cost of transfer would increase by 22k.

I think this should finally work:

if (to != address(0) && numCheckpoints(to) == 0 && delegates(to) == address(0)) {
    _delegate(to, to);
 }

Thought ?

I think that solution is good enough!

1 Like