Overhead of ERC20Votes Transfer

I am interested in the overhead in the transfer function of ERC20Votes compared to ERC20. This is caused by calling _moveVotingPower(delegates(from), delegates(to), amount) in the _afterTokenTransfer function. Even if the sender and receiver have never delegated or only 0 tokens are transferred, the transfer is more expensive than a normal ERC20 transfer. My question is what is the minimum additional cost?

In the minimum case (e.g. amount = 0), essentially only delegates(from) and delegates(to) have to be called and passed as parameters together with amount. Afterwards, only a simple comparison in _moveVotingPower has to be carried out.

Hello @tom4543

Right now, the ERC20Vote implementation will do the delegate lookup for the from and to accounts, even if the transfer amount is 0. This includes two sload, so the total cost is ~4.2k gas.

If both delegate are equal OR if the value transferred is 0 OR if neither account as a delegate ... then that is basically it.

Otherwise we must push or update a checkpoint for each delegate. Worst case scenario is creating 2 new checkpoints and updating the array lengths, so ~52k gas

How did you get 52k? It looks like a typo, it's much less than that.
I was wrong, updated below.

In the worst case, in which both source and destination token holders have delegates, _moveVotingPower will emit for each of them an event with two topics (selector + indexed argument) and do the following storage operations:

  • checkpoints[delegate].length: read + read + write (nz→nz except very first checkpoint)
  • checkpoints[delegate][last]: read
  • checkpoints[delegate][last + 1]: write (z→nz)

So: an event (~1500) plus 3 storage accesses (3 x 2000) plus 3 storage reads (3 x 100) plus a nz→nz storage write (2900) plus a z→nz storage write (20000), all of that times 2, which adds up to 61.4k.

Additionally you first read both delegates which as mentioned is 4.2k gas.

In total you get 65.6k gas worst case overhead.

If one of the two accounts does not have a delegate, it goes down to 34.9k.

If neither has a delegate, just 4.2k.

Not counting the storage accesses (3x2000), that first write is 3k, the second write is 20k. The sstore alone are 46k gas. storage operation are only 100 when you read or write to a slot that has already been written to in the same tx.

For an account that already has some checkpoints:

  • Reading who the delegate is for an account: 2k gas (cold sload)
  • Reading the length of the array: 2k gas (cold sload)
  • Reading the last checkpoint, to check if we can reuse it or if we need to overwrite it: 2k gas (cold sload)
  • Writting a new checkpoint on an empty slot: 22k gas (sstore from 0 on cold slot)
  • Updating the length of the array: 3k gas (sstore from non 0 on a hot slot)

That is 31k gas per account.

You have to update sender and recipient, so that is 62k gas

Note that if one of the account doesn't have any checkpoint yet, you don't have to pay one of the sloads, but the "updating the length of the array" would cost 20k (sstore from non 0) ...

Thanks, I updated my numbers above because they were misleading.