Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve ERC20Snapshot documentation #2186

Merged
merged 7 commits into from
Apr 14, 2020
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions contracts/token/ERC20/ERC20Snapshot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,36 @@ import "../../utils/Counters.sol";
import "./ERC20.sol";

/**
* @dev ERC20 token with snapshots.
* @dev This contract extends an ERC20 token with a snapshot mechanism. When a snapshot is created, the balances and
* total supply at the time are recorded for later access.
*
* When a snapshot is made, the balances and total supply at the time of the snapshot are recorded for later
* access.
* This can be used to safely create mechanisms based on token balances such as trustless dividends or weighted voting.
* In naive implementations it's possible to perform a "double spend" attack by reusing the same balance from different
* accounts. By using snapshots to calculate dividends or voting power, those attacks no longer apply. It can also be
* used to create an efficient ERC20 forking mechanism.
*
* To make a snapshot, call the {snapshot} function, which will emit the {Snapshot} event and return a snapshot id.
* To get the total supply from a snapshot, call the function {totalSupplyAt} with the snapshot id.
* To get the balance of an account from a snapshot, call the {balanceOfAt} function with the snapshot id and the
* account address.
* @author Validity Labs AG <info@validitylabs.org>
* Snapshots are created by the internal {_snapshot} function, which will emit the {Snapshot} event and return a
* snapshot id. To get the total supply at the time of a snapshot, call the function {totalSupplyAt} with the snapshot
* id. To get the balance of an account at the time of a snapshot, call the {balanceOfAt} function with the snapshot id
* and the account address.
*
* {_snapshot} is internal so that you can decide if and how to expose it externally. If it is exposed, it can be
* guarded by something like {AccessControl} or open to the public. The latter is required for implementing certain
* trust minimization mechanisms such as forking, but you must consider that it can potentially be used by attackers in
* two ways. First, it can be used to increase the cost of retrieval of values from snapshots, although it will grow
* logarithmically thus rendering this attack ineffective in the long term. Second, it can be used to target specific
* accounts and increase the cost of ERC20 transfers for them. (We haven't measured the actual numbers. If this is
* something you're interested in please reach out to us.)
*
* ==== Gas Costs
*
* Snapshots are efficient. Snapshot creation is _O(1)_. Retrieval of balances or total supply from a snapshot is _O(log
* n)_ in the number of snapshots that have been created, although _n_ for a specific account will generally be much
* smaller since identical balances in subsequent snapshots are stored as a single entry.
*
* There is a constant overhead for normal ERC20 transfers due to the additional snapshot bookkeeping. This overhead is
* only significant for the first transfer that immediately follows a snapshot for a particular account. Subsequent
* transfers will have normal cost until the next snapshot, and so on.
*/
abstract contract ERC20Snapshot is ERC20 {
// Inspired by Jordi Baylina's MiniMeToken to record historical balances:
Expand All @@ -38,12 +58,13 @@ abstract contract ERC20Snapshot is ERC20 {
// Snapshot ids increase monotonically, with the first value being 1. An id of 0 is invalid.
Counters.Counter private _currentSnapshotId;

/**
* @dev Emitted by {_snapshot} when a snapshot is created. Contains the snapshot id that can be used for retrieval.
frangio marked this conversation as resolved.
Show resolved Hide resolved
*/
event Snapshot(uint256 id);

/**
* @dev Creates a new snapshot id. Balances are only stored in snapshots on demand: unless a snapshot was taken, a
* balance change will not be recorded. This means the extra added cost of storing snapshotted balances is only paid
* when required, but is also flexible enough that it allows for e.g. daily snapshots.
* @dev Creates a new snapshot and returns its snapshot id. Emits a {Snapshot} event that contains the same id.
frangio marked this conversation as resolved.
Show resolved Hide resolved
*/
function _snapshot() internal virtual returns (uint256) {
_currentSnapshotId.increment();
Expand All @@ -53,12 +74,18 @@ abstract contract ERC20Snapshot is ERC20 {
return currentId;
}

/**
* @dev Retrieves the balance of `account` at the time `snapshotId` was created.
*/
function balanceOfAt(address account, uint256 snapshotId) public view returns (uint256) {
(bool snapshotted, uint256 value) = _valueAt(snapshotId, _accountBalanceSnapshots[account]);

return snapshotted ? value : balanceOf(account);
}

/**
* @dev Retrieves the total supply at the time `snapshotId` was created.
*/
function totalSupplyAt(uint256 snapshotId) public view returns(uint256) {
(bool snapshotted, uint256 value) = _valueAt(snapshotId, _totalSupplySnapshots);

Expand Down