فهرست منبع

ERC20 Snapshot Impl #2 (#1617)

* ✏️ 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
Matt Swezey 6 سال پیش
والد
کامیت
40d15146c4
5فایلهای تغییر یافته به همراه351 افزوده شده و 2 حذف شده
  1. 2 2
      .gitignore
  2. 1 0
      CHANGELOG.md
  3. 133 0
      contracts/drafts/ERC20Snapshot.sol
  4. 18 0
      contracts/mocks/ERC20SnapshotMock.sol
  5. 197 0
      test/drafts/ERC20Snapshot.test.js

+ 2 - 2
.gitignore

@@ -32,8 +32,8 @@ npm-debug.log
 # truffle build directory
 build/
 
-# lol macs
-.DS_Store/
+# macOS
+.DS_Store
 
 # truffle
 .node-xmlhttprequest-*

+ 1 - 0
CHANGELOG.md

@@ -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 - 0
contracts/drafts/ERC20Snapshot.sol

@@ -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 - 0
contracts/mocks/ERC20SnapshotMock.sol

@@ -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 - 0
test/drafts/ERC20Snapshot.test.js

@@ -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)
+              );
+            }
+          });
+        });
+      });
+    });
+  });
+});