top of page
  • Writer's pictureJeffrey Scholz

ERC20 Snapshot

Updated: Feb 28

ERC20 Snapshot solves the problem of double voting.

If votes are weighed by the number of tokens someone holds, then a malicious actor can use their tokens to vote, then transfer the tokens to another address, vote with that, and so forth. If each address is a smart contract, then the hacker can accomplish all these votes in one transaction. A related attack is to use a flashloan to obtain a bunch of governance tokens, vote, then return the flash loan.

A similar problem exists for claiming airdrops. One could use their ERC20 tokens to claim an airdrop, then transfer their tokens to another address, then claim the airdrop again.

Fundamentally, ERC20 snapshot provides a mechanism to defend against users transfering tokens and re-using token utility in the same transaction.

At first, snapshotting might seem like an intractible problem. The brute force, or naive, solution to this is to iterate through every address in the “balances” mapping of ERC20, then copy these to another mapping. It isn’t possible to natively iterate through a mapping in ERC20, so the coder would have to use an enumerable map — a map with an array that keeps track of all the keys.

As one can imagine, this O(n) operation would be extremely gas intensive.

There is a saying in computer science that “Every problem in computer science can be solved with another level of indirection” and that’s how ERC20 snapshots solves it.

Efficient but Naive Solution

Let’s take the example of the balances mapping.

Here is a buggy solidity solution, but a step in the right direction.


In this case, snapshotNumber is a counter that starts at zero and increments by one every time a snapshot is done.

Going back to our voting example, we create a snapshot at a particular point in time, let everyone go about their business, then create another snapshot. At the time of voting, we use the previous snapshot since the current snapshot can still be changed by transfering tokens.

This way, we can query someone’s balance by supplying both the snapshotNumber and their address at the snapshot we care about. Since we know the current snapshot, balanceOf is simply the balances at the most recent snapshot.

Ah, but there is a problem! Every time we do a snapshot, everyone’s balances are set to zero! It is possible to solve this with some accounting — just track the last snapshot the user transacted at, but this quickly gets complicated as the engineer tries to cover all the corner cases.

Openzeppelin solution

This is how OpenZeppelin accomplishes it. (code)

Each balance stores a struct

struct Snapshots {
    uint256[] ids;
    uint256[] values;

mapping(address => Snapshots) private _accountBalanceSnapshots;

function balanceOfAt(address account, uint256 snapshotId) public view virtual returns (uint256) {
    (bool snapshotted, uint256 value) = _valueAt(snapshotId, _accountBalanceSnapshots[account]);

    return snapshotted ? value : balanceOf(account);

Inside the user balances, we store a struct which has an array of ids and values. The array of ids is a monotonically increasing snapshot id, and the values are the balance when that id was the active snapshot.

When a token is transferred to an address, whether by minting or by a regular transfer, the transfer function compares the current snapshot id to the latest one seen in the “ids” field.

If the address had not previously received a transfer during the current snapshot, then the arrays “ids” and “values” get appended with the latest snapshot id and the appropriate account balance.

Note that we cannot assume the “ids” for an arbitrary address increase by one. The snapshot ids increment by one, but if an address never receives or sends tokens during a particular snapshot epoch (I.e the account is dormant), then the ids array for that address will not contain that snapshot id.

Because of this, we cannot access an accounts balance at a snapshot by doing “ids[snapshotId]”. Instead, binary search is used to find the snapshot id the user is requesting. If the id is not found, then we use the previous adjacent snapshot value. For example, if we want to know a user’s balance at snapshot 5, but they didn’t transfer tokens during snapshots 3 and 4, we would look at snapshot 2.

Total supply is tracked the same way

The reader may note that the struct Snapshots has seemingly overgeneric variable names, like ids and values. Shouldn’t it just be named “balance” to be more precise?

ERC20 Snapshot keeps track of the total supply using the same strategy, so the variable names capture the fact that the same struct is used both for tracking user balances and the total supply.

Only mint and burn change the total supply, so when these functions are invoked, the struct storing the total supply is checked to see if the snapshot has changed before updating these values.

Note that historic allowance values are not snapshotted.

Added gas cost

Regular transfers are more expensive because we check if the last id in the ids of the user matches the current snapshot and add a new id if that is not the case. Appending to the array of ids and values will incur two extra SSTOREs. When a new snapshot occures, the first transfer to or from an address will be more expensive. But the second transaction will cost approximately the same as a transfer in a regular ERC20 token.

Getting hacked

When a snapshot is created, the previous snapshot taken becomes the frozen record. Snapshots are forward looking, not backwards looking. Let’s say the current snapshot id is 3, and we take a snaphot on April 5th, 2024. The active snapshot is snapshot 4. If a community conducts a vote using snapshot 4 on say April 6th, this is still vulnerable to the hack described above. Within snapshot 4, token owners can vote then transfer, and their balances will change. It’s the balances in snapshot 3 that cannot change.

Tallying votes

This is simply the balance of an address divided by the total supply, all at a particular snapshot.

Learn more

This blog post is part of the learning material in our DeFi bootcamp, the third in our Ethereum blockchain bootcamp.

110 views0 comments

Recent Posts

See All

Verify Signature Solidity in Foundry. Here is a minimal (copy and paste) example of how to safely create and verify ECDSA signatures

bottom of page