ERC20Snapshot Alternative in new OZ Library

@Recursive , Have you looked over this documentation for how to implement your SnapShot?

Hey Sam,
The new OZ libraries doesn’t have erc20snapshot anymore. That’s the problem

Hi @Recursive,

I have the similar questions and am also having difficulties finding the right answers. Apparently, ERC20Votes is somewhat considered the successor of ERC20Snapshots.
From reading the code, it appears there is no need to manually create a snapshot/checkpoint anymore. Instead, each token movement automatically creates a snapshot/checkpoint, so that all historical balances (at the end of a block) are preserved. (The only exception from that is if multiple transactions take place in the same block.)

I really wonder about the gas consumption of this. It takes a lot of storage to preserve every change, even though several tweaks are used to reduce gas cost.

Also, lookups of old voting power are now binary searches, which sounds really expensive.

So, yes, ERC20Votes could be used as replacement for ERC20Snapshots. But to me it does not look like a very good replacement.

Please keep me posted if you find out more!

Hey brother.
Thank you for your reply. I am considering to keep using the old version of OZ library to be able to use erc20snapshot to be honest. Which is 4.9 version.

Somehow I couldn’t find the historical balance check on the erc20votes.
You got any deployed contracts using erc20votes so that I can check the read functions on chain

Hi,

here is the lookup function: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v5.0/contracts/governance/utils/Votes.sol#L89

I have not used it yet.

Hey man, it says getPastVotes, votes means balance ?

Yes, the implementation works by defining token balance as voting power.

It also says the votes need to be delegated and not doing so would save gas. I don't fully understand that part yet. I have not seen how accounting of a token transfer changes depending on delegation.

Do you have any contract addresses that you know using ercvotes? I might check the balance change of it

I don't have an example, sorry. But if I wanted to understand it better, I would create an example in my dev environment and interact with it.
We skipped the talk about the basics:

  • What is the task you are trying to solve?
  • What is your background/skillset regarding smart contract development?

I am currently developing a smart contract that will mint and redeem tokens and at each mint and redeem we will airdrop some erc20 tokens to certain token holders at that time as reflections.

Doing reflections are not ideal on chain transactions as inline since the number of holders can get bigger and bigger causing revert.
So we are moving the reflections to backend.

I was aiming to create snapshots on the mints and redeems and create a event on the transaction with snapshot id and info for being used on backend by listening events and calculating the rewards based on the events snapshot id

Can you elaborate on "reflections"? I am unsure what it means in this context.

But, from an outside perspective:

  1. airdropping tokens to many recipients with each "mint and redeem" tx will be prohibitively expensive if there are a number of recipients. Possibly better: Store `numberOfTokensMintedAndRedeemedSinceLastAirdrop" somewhere, and use it to airdrop once in a while.
  2. If you really want to go the other route, I don't really see why you would need Snapshots or Votes. Listen to the mint events, and react to that in the backend. This means it is not provable on-chain though.

The reason why we recommend ERC20Votes is that the "snapshot" mechanisms were abstracted away in multiple layers. This is its current inheritance graph:

The main difference is that the contract inherits from Votes, where all the logic for snapshotting happens. Given that the contract is built for governance purposes, I see there are a few important differences that makes it behave differently to ERC20Snapshot:

To replace the ERC20Snapshot behavior, you'll need to configure the contract clock (EIP-6372) to use an _snapshotId instead, and override Votes's delegates(address account) function to make each address delegate to themselves:

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

Here's an example:

Although I recognize there are a few extra things you may not need (eg. EIP5805, EIP712, Votes), the gas overhead you mention will be only during deployment. If the contract remains used only for snapshotting purposes, I'd expect around the same gas costs in runtime.

If you really want to reimplement ERC20Snapshot, that's possible using the Checkpoints but I'd keep recommending using EIP-6372 for identifying your contract clock.

Here's an example of a reimplementation:

:warning: WARNING: The referenced code is not audited and is only presented as a pointer for you to evaluate the options.

2 Likes

We need to consider it.

I took your idea and modified it a bit. Now the only problem left is lowerLookup function from CheckPoints structs

Note that the last two functions are redundant, and I use them only for test.

I modified the Checkpoints library to return an additional bool param indicating if the value is found or not.

function lowerLookup(
        Trace208 storage self,
        uint48 key
    ) internal view returns (bool, uint208) {
        uint256 len = self._checkpoints.length;
        uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, len);
        return
            pos == len
                ? (false, 0)
                : (true, _unsafeAccess(self._checkpoints, pos)._value);
    }

Thank you for these hints, they are much appreciated!

1 Like

2**208 is more than enough. Even with an ERC20Votes that uses 18 decimals, you would have a buffer of (2**208)/1e18 = 4.11e44 for total supply. That's huge. Also this allows to save gas by packing the checkpoint id with its corresponding supply in a single slot (uint48 + uint208)

1 Like

@ernestognw
What if I want to have ERC20Checkpoint and ERC20Votes extensions simultaneously. I won't be able to override two different clock() functions to return on the one hand current snapshotId and Time.blockNumber() on the other hand because clock() can return only one uint48 value.

One way to solve it is to use some getCurrentSnapshotId instead of clock() and define only CLOCK_MODE or even just forget about IERC6372 for ERC20Checkpoint

Also regaring this I will need to comment (see the line)

// Update balance and/or total supply chekpoints before the values are modified. This is implemented
    // in the _update function, which is executed for _mint, _burn, and _transfer operations.
    function _update(address from, address to, uint256 value) internal virtual override {
        if (value > type(uint208).max) {
            revert SafeCast.SafeCastOverflowedUintDowncast(208, value);
        }

        if (from == address(0)) {
            // mint
            _updateAccountCheckpoint(to);
            _updateTotalSupplyCheckpoint();
        } else if (to == address(0)) {
            // burn
            _updateAccountCheckpoint(from);
            _updateTotalSupplyCheckpoint();
        } else {
            // transfer
            _updateAccountCheckpoint(from);
            _updateAccountCheckpoint(to);
        }

        // super._update(from, to, value); THAT LINE HERE
    }

Because super._update i.e. ERC20._update is already called in ERC20Votes to transfer voting power units after the tokens transfer.

Then in MyToken which inherits from ERC20Votes and ERC20Checkpoints I will need to write such an override

function _update(
        address from,
        address to,
        uint256 value
    ) internal virtual override(ERC20, ERC20Votes, ERC20Checkpoint) {
        ERC20Checkpoint._update(from, to, value);
        ERC20Votes._update(from, to, value);
    }

Do you have any suggestions regarding this?

What if I want to have ERC20Checkpoint and ERC20Votes extensions simultaneously.

Yeah, not sure about this. I wouldn't feel comfortable mixing them up. Generally, I'd prefer to keep each contract with a single responsibility. Though I recognize it may depend on the use case.

Then in MyToken which inherits from ERC20Votes and ERC20Checkpoints I will need to write such an override

I'd suggest studying how Solidity supports multiple inheritance and polymorfism. If two contracts implement the same function, their execution order will be defined by a linearization algorithm.

In the current override you're adding, you'll be calling ERC20's _update function twice.

My advice is to use a super._update() instead, it will call the functions one level higher up in the flattened inheritance hierarchy.

I'd suggest looking at your different _update() functions and see if there's a reason why order execution should matter. In case there's a reason, you should select the execution order by changing the inheritance order.

Some examples:

// _updates()'s are executed in the following order:
//  1 - Base1
//  2 - Base2
//  3 - Derived1
contract Derived1 is Base1, Base2 {
    _update() { super._update() }
}

// _update()'s are executed in the following order:
//  1 - Base2
//  2 - Base1
//  3 - Derived2
contract Derived2 is Base2, Base1 {
    _update() { super._update() }
}