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