In my smart contract, I want to take a snapshot time to time to save all the holders balances for future usage.
I was aiming to use ERC20Snapshot but I can see that it has been removed.
From my research, some people say that ERC20Votes can be used for this purpose but I really did not understand how ERC20Votes can be used to take snapshots for holder balances and retrieve them later on.
Can someone please explain me how to use ERC20Votes for this purpose or is there any alternative to ERC20Snapshot that I can use with the new OZ Libraries.
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.
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
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.
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:
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.
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:
The "snapshots" (now called checkpoints) depend on the EIP-6372 contract clock, as opposed to an snapshotId
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: The referenced code is not audited and is only presented as a pointer for you to evaluate the options.
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)
@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);
}