Back to Lessons
Lesson 14

Balance Snapshots for Airdrops & Dividends

+25EFFORT
5 sections
balanceOf()
01
text

Airdrops, dividend distributions, and some votes need everyone's balance frozen at a single moment, so people cannot game eligibility by moving tokens afterward. Historically OpenZeppelin offered ERC20Snapshot; modern guidance has shifted to checkpoint-based history with a clock (ERC-6372). Both let you read a balance 'as of' a past point.

02
code
solidity
1// Classic ERC20Snapshot interface:
2function snapshot() external returns (uint256 id); // restricted
3function balanceOfAt(address account, uint256 snapshotId)
4 external view returns (uint256);
5function totalSupplyAt(uint256 snapshotId)
6 external view returns (uint256);
03
text

The implementation is lazy and gas-aware: taking a snapshot just records a new id. A balance is only stored for that id on the FIRST transfer an account makes after the snapshot (capturing its pre-transfer value). Reads via balanceOfAt then return that frozen number. OpenZeppelin now recommends the checkpoint approach (like ERC20Votes) paired with an ERC-6372 clock that can tick by block number or timestamp.

04
note
Key Insight

Gas note: the first transfer by each holder after a snapshot pays an extra SSTORE to record the historical balance. Avoid triggering snapshots inside hot loops, and prefer off-chain Merkle airdrops (publish a root, let users claim) when you only need eligibility, not on-chain historical reads.

05
code
javascript
1// Distribute an airdrop proportional to balances at a snapshot
2const snapId = await token.read.getCurrentSnapshotId();
3const bal = await token.read.balanceOfAt([user, snapId]);
4const supply = await token.read.totalSupplyAt([snapId]);
5// share = rewardPool * bal / supply
Complete

Connect your wallet to mark this lesson as complete

Earn 25 EFFORT tokens