* ✏️ Refactor code & Refork OZ Repo
* Refactor ERC20Snapshot to use on-demand snapshots.
* Add ERC20Snapshot changelog entry.
* Move ERC20Snapshot to drafts.
* Improve changelog entry.
* Make snapshot tests clearer.
* Refactor ERC20Snapshots to use Counters.
* Refactor snapshot arrays into a struct.
* Remove .DS_Store files.
* Delete yarn.lock
* Fix linter error.
* simplify gitignore entry
This commit is contained in:
committed by
Francisco Giordano
parent
2c34cfbe0e
commit
40d15146c4
4
.gitignore
vendored
4
.gitignore
vendored
@ -32,8 +32,8 @@ npm-debug.log
|
||||
# truffle build directory
|
||||
build/
|
||||
|
||||
# lol macs
|
||||
.DS_Store/
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# truffle
|
||||
.node-xmlhttprequest-*
|
||||
|
||||
@ -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.
|
||||
|
||||
133
contracts/drafts/ERC20Snapshot.sol
Normal file
133
contracts/drafts/ERC20Snapshot.sol
Normal file
@ -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
contracts/mocks/ERC20SnapshotMock.sol
Normal file
18
contracts/mocks/ERC20SnapshotMock.sol
Normal file
@ -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
test/drafts/ERC20Snapshot.test.js
Normal file
197
test/drafts/ERC20Snapshot.test.js
Normal file
@ -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)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user