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

ERC20 Snapshot Impl #2 #1617

Merged
merged 14 commits into from
Feb 6, 2019
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ npm-debug.log
# truffle build directory
build/

# lol macs
.DS_Store/
# macOS
.DS_Store

# truffle
.node-xmlhttprequest-*
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### New features:
* `ERC20`: added internal `_approve(address owner, address spender, uint256 value)`, allowing derived contracts to set the allowance of arbitrary accounts.
* `ERC20Metadata`: added internal `_setTokenURI(string memory tokenURI)`.
* `ERC20Snapshot`: create snapshots on demand of the token balances and total supply, to later retrieve and e.g. calculate dividends at a past time.

### Improvements:
* Upgraded the minimum compiler version to v0.5.2: this removes many Solidity warnings that were false positives.
Expand Down
133 changes: 133 additions & 0 deletions contracts/drafts/ERC20Snapshot.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
pragma solidity ^0.5.2;

import "../math/SafeMath.sol";
import "../utils/Arrays.sol";
import "../drafts/Counters.sol";
import "../token/ERC20/ERC20.sol";

/**
* @title ERC20 token with snapshots.
* inspired by Jordi Baylina's MiniMeToken to record historical balances
* Snapshots store a value at the time a snapshot is taken (and a new snapshot id created), and the corresponding
* snapshot id. Each account has individual snapshots taken on demand, as does the token's total supply.
* @author Validity Labs AG <info@validitylabs.org>
*/
contract ERC20Snapshot is ERC20 {
using SafeMath for uint256;
using Arrays for uint256[];
using Counters for Counters.Counter;

// Snapshoted values have arrays of ids and the value corresponding to that id. These could be an array of a
// Snapshot struct, but that would impede usage of functions that work on an array.
struct Snapshots {
uint256[] ids;
uint256[] values;
}

mapping (address => Snapshots) private _accountBalanceSnapshots;
Snapshots private _totalSupplySnaphots;

// Snapshot ids increase monotonically, with the first value being 1. An id of 0 is invalid.
Counters.Counter private _currentSnapshotId;

event Snapshot(uint256 id);

// 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.
function snapshot() public returns (uint256) {
_currentSnapshotId.increment();

uint256 currentId = _currentSnapshotId.current();
emit Snapshot(currentId);
return currentId;
}

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

return snapshotted ? value : balanceOf(account);
}

function totalSupplyAt(uint256 snapshotId) public view returns(uint256) {
(bool snapshotted, uint256 value) = _valueAt(snapshotId, _totalSupplySnaphots);

return snapshotted ? value : totalSupply();
}

// _transfer, _mint and _burn are the only functions where the balances are modified, so it is there that the
// snapshots are updated. Note that the update happens _before_ the balance change, with the pre-modified value.
// The same is true for the total supply and _mint and _burn.
function _transfer(address from, address to, uint256 value) internal {
_updateAccountSnapshot(from);
_updateAccountSnapshot(to);

super._transfer(from, to, value);
}

function _mint(address account, uint256 value) internal {
_updateAccountSnapshot(account);
_updateTotalSupplySnapshot();

super._mint(account, value);
}

function _burn(address account, uint256 value) internal {
_updateAccountSnapshot(account);
_updateTotalSupplySnapshot();

super._burn(account, value);
}

// When a valid snapshot is queried, there are three possibilities:
// a) The queried value was not modified after the snapshot was taken. Therefore, a snapshot entry was never
// created for this id, and all stored snapshot ids are smaller than the requested one. The value that corresponds
// to this id is the current one.
// b) The queried value was modified after the snapshot was taken. Therefore, there will be an entry with the
// requested id, and its value is the one to return.
// c) More snapshots were created after the requested one, and the queried value was later modified. There will be
// no entry for the requested id: the value that corresponds to it is that of the smallest snapshot id that is
// larger than the requested one.
//
// In summary, we need to find an element in an array, returning the index of the smallest value that is larger if
// it is not found, unless said value doesn't exist (e.g. when all values are smaller). Arrays.findUpperBound does
// exactly this.
function _valueAt(uint256 snapshotId, Snapshots storage snapshots)
private view returns (bool, uint256)
{
require(snapshotId > 0);
require(snapshotId <= _currentSnapshotId.current());

uint256 index = snapshots.ids.findUpperBound(snapshotId);

if (index == snapshots.ids.length) {
return (false, 0);
} else {
return (true, snapshots.values[index]);
}
}

function _updateAccountSnapshot(address account) private {
_updateSnapshot(_accountBalanceSnapshots[account], balanceOf(account));
}

function _updateTotalSupplySnapshot() private {
_updateSnapshot(_totalSupplySnaphots, totalSupply());
}

function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private {
uint256 currentId = _currentSnapshotId.current();
if (_lastSnapshotId(snapshots.ids) < currentId) {
snapshots.ids.push(currentId);
snapshots.values.push(currentValue);
}
}

function _lastSnapshotId(uint256[] storage ids) private view returns (uint256) {
if (ids.length == 0) {
return 0;
} else {
return ids[ids.length - 1];
}
}
}
18 changes: 18 additions & 0 deletions contracts/mocks/ERC20SnapshotMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
pragma solidity ^0.5.2;

import "../drafts/ERC20Snapshot.sol";


contract ERC20SnapshotMock is ERC20Snapshot {
constructor(address initialAccount, uint256 initialBalance) public {
_mint(initialAccount, initialBalance);
}

function mint(address account, uint256 amount) public {
_mint(account, amount);
}

function burn(address account, uint256 amount) public {
_burn(account, amount);
}
}
197 changes: 197 additions & 0 deletions test/drafts/ERC20Snapshot.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
const { BN, expectEvent, shouldFail } = require('openzeppelin-test-helpers');
const ERC20SnapshotMock = artifacts.require('ERC20SnapshotMock');

contract('ERC20Snapshot', function ([_, initialHolder, recipient, anyone]) {
const initialSupply = new BN(100);

beforeEach(async function () {
this.token = await ERC20SnapshotMock.new(initialHolder, initialSupply);
});

describe('snapshot', function () {
it('emits a snapshot event', async function () {
const { logs } = await this.token.snapshot();
expectEvent.inLogs(logs, 'Snapshot');
});

it('creates increasing snapshots ids, starting from 1', async function () {
for (const id of ['1', '2', '3', '4', '5']) {
const { logs } = await this.token.snapshot();
expectEvent.inLogs(logs, 'Snapshot', { id });
}
});
});

describe('totalSupplyAt', function () {
it('reverts with a snapshot id of 0', async function () {
await shouldFail.reverting(this.token.totalSupplyAt(0));
});

it('reverts with a not-yet-created snapshot id', async function () {
await shouldFail.reverting(this.token.totalSupplyAt(1));
});

context('with initial snapshot', function () {
beforeEach(async function () {
this.initialSnapshotId = new BN('1');

const { logs } = await this.token.snapshot();
expectEvent.inLogs(logs, 'Snapshot', { id: this.initialSnapshotId });
});

context('with no supply changes after the snapshot', function () {
it('returns the current total supply', async function () {
(await this.token.totalSupplyAt(this.initialSnapshotId)).should.be.bignumber.equal(initialSupply);
});
});

context('with supply changes after the snapshot', function () {
beforeEach(async function () {
await this.token.mint(anyone, new BN('50'));
await this.token.burn(initialHolder, new BN('20'));
});

it('returns the total supply before the changes', async function () {
(await this.token.totalSupplyAt(this.initialSnapshotId)).should.be.bignumber.equal(initialSupply);
});

context('with a second snapshot after supply changes', function () {
beforeEach(async function () {
this.secondSnapshotId = new BN('2');

const { logs } = await this.token.snapshot();
expectEvent.inLogs(logs, 'Snapshot', { id: this.secondSnapshotId });
});

it('snapshots return the supply before and after the changes', async function () {
(await this.token.totalSupplyAt(this.initialSnapshotId)).should.be.bignumber.equal(initialSupply);

(await this.token.totalSupplyAt(this.secondSnapshotId)).should.be.bignumber.equal(
await this.token.totalSupply()
);
});
});

context('with multiple snapshots after supply changes', function () {
beforeEach(async function () {
this.secondSnapshotIds = ['2', '3', '4'];

for (const id of this.secondSnapshotIds) {
const { logs } = await this.token.snapshot();
expectEvent.inLogs(logs, 'Snapshot', { id });
}
});

it('all posterior snapshots return the supply after the changes', async function () {
(await this.token.totalSupplyAt(this.initialSnapshotId)).should.be.bignumber.equal(initialSupply);

const currentSupply = await this.token.totalSupply();

for (const id of this.secondSnapshotIds) {
(await this.token.totalSupplyAt(id)).should.be.bignumber.equal(currentSupply);
}
});
});
});
});
});

describe('balanceOfAt', function () {
it('reverts with a snapshot id of 0', async function () {
await shouldFail.reverting(this.token.balanceOfAt(anyone, 0));
});

it('reverts with a not-yet-created snapshot id', async function () {
await shouldFail.reverting(this.token.balanceOfAt(anyone, 1));
});

context('with initial snapshot', function () {
beforeEach(async function () {
this.initialSnapshotId = new BN('1');

const { logs } = await this.token.snapshot();
expectEvent.inLogs(logs, 'Snapshot', { id: this.initialSnapshotId });
});

context('with no balance changes after the snapshot', function () {
it('returns the current balance for all accounts', async function () {
(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId))
.should.be.bignumber.equal(initialSupply);
(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).should.be.bignumber.equal('0');
(await this.token.balanceOfAt(anyone, this.initialSnapshotId)).should.be.bignumber.equal('0');
});
});

context('with balance changes after the snapshot', function () {
beforeEach(async function () {
await this.token.transfer(recipient, new BN('10'), { from: initialHolder });
await this.token.mint(recipient, new BN('50'));
await this.token.burn(initialHolder, new BN('20'));
});

it('returns the balances before the changes', async function () {
(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId))
.should.be.bignumber.equal(initialSupply);
(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).should.be.bignumber.equal('0');
(await this.token.balanceOfAt(anyone, this.initialSnapshotId)).should.be.bignumber.equal('0');
});

context('with a second snapshot after supply changes', function () {
beforeEach(async function () {
this.secondSnapshotId = new BN('2');

const { logs } = await this.token.snapshot();
expectEvent.inLogs(logs, 'Snapshot', { id: this.secondSnapshotId });
});

it('snapshots return the balances before and after the changes', async function () {
(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId))
.should.be.bignumber.equal(initialSupply);
(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).should.be.bignumber.equal('0');
(await this.token.balanceOfAt(anyone, this.initialSnapshotId)).should.be.bignumber.equal('0');

(await this.token.balanceOfAt(initialHolder, this.secondSnapshotId)).should.be.bignumber.equal(
await this.token.balanceOf(initialHolder)
);
(await this.token.balanceOfAt(recipient, this.secondSnapshotId)).should.be.bignumber.equal(
await this.token.balanceOf(recipient)
);
(await this.token.balanceOfAt(anyone, this.secondSnapshotId)).should.be.bignumber.equal(
await this.token.balanceOf(anyone)
);
});
});

context('with multiple snapshots after supply changes', function () {
beforeEach(async function () {
this.secondSnapshotIds = ['2', '3', '4'];

for (const id of this.secondSnapshotIds) {
const { logs } = await this.token.snapshot();
expectEvent.inLogs(logs, 'Snapshot', { id });
}
});

it('all posterior snapshots return the supply after the changes', async function () {
(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId))
.should.be.bignumber.equal(initialSupply);
(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).should.be.bignumber.equal('0');
(await this.token.balanceOfAt(anyone, this.initialSnapshotId)).should.be.bignumber.equal('0');

for (const id of this.secondSnapshotIds) {
(await this.token.balanceOfAt(initialHolder, id)).should.be.bignumber.equal(
await this.token.balanceOf(initialHolder)
);
(await this.token.balanceOfAt(recipient, id)).should.be.bignumber.equal(
await this.token.balanceOf(recipient)
);
(await this.token.balanceOfAt(anyone, id)).should.be.bignumber.equal(
await this.token.balanceOf(anyone)
);
}
});
});
});
});
});
});