Skip to content

Commit

Permalink
improve snapshoting
Browse files Browse the repository at this point in the history
  • Loading branch information
Amxx committed Mar 24, 2021
1 parent 10a5b63 commit 512324a
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 122 deletions.
21 changes: 0 additions & 21 deletions contracts/token/ERC20/extensions/ERC20Snapshot.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,25 +75,4 @@ abstract contract ERC20Snapshot is ERC20SnapshotEveryBlock {
function _getCurrentSnapshotId() internal view virtual override returns (uint256) {
return _currentSnapshotId.current();
}

/**
* @dev Retrieves the balance of `account` at the time `snapshotId` was created.
*/
function balanceOfAt(address account, uint256 snapshotId) public view virtual override returns (uint256) {
require(snapshotId > 0, "ERC20Snapshot: id is 0");
// solhint-disable-next-line max-line-length
require(snapshotId <= _getCurrentSnapshotId(), "ERC20Snapshot: nonexistent id");
return super.balanceOfAt(account, snapshotId);
}

/**
* @dev Retrieves the total supply at the time `snapshotId` was created.
*/
function totalSupplyAt(uint256 snapshotId) public view virtual override returns(uint256) {
require(snapshotId > 0, "ERC20Snapshot: id is 0");
// solhint-disable-next-line max-line-length
require(snapshotId <= _getCurrentSnapshotId(), "ERC20Snapshot: nonexistent id");
return super.totalSupplyAt(snapshotId);
}

}
5 changes: 4 additions & 1 deletion contracts/token/ERC20/extensions/ERC20SnapshotEveryBlock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ abstract contract ERC20SnapshotEveryBlock is ERC20 {
* @dev Get the current snapshotId
*/
function _getCurrentSnapshotId() internal view virtual returns (uint256) {
return block.number - 1;
return block.number;
}

/**
Expand Down Expand Up @@ -92,6 +92,9 @@ abstract contract ERC20SnapshotEveryBlock is ERC20 {
function _valueAt(uint256 snapshotId, Snapshots storage snapshots)
private view returns (bool, uint256)
{
require(snapshotId > 0, "ERC20Snapshot: id is 0");
require(snapshotId <= _getCurrentSnapshotId(), "ERC20Snapshot: nonexistent id");

// 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
Expand Down
2 changes: 1 addition & 1 deletion hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const enableProduction = process.env.COMPILE_MODE === 'production';
*/
module.exports = {
solidity: {
version: '0.8.0',
version: '0.8.3',
settings: {
optimizer: {
enabled: enableGasReport || enableProduction,
Expand Down
269 changes: 170 additions & 99 deletions test/token/ERC20/extensions/ERC20SnapshotEveryBlock.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { BN, time } = require('@openzeppelin/test-helpers');
const { BN, time, expectRevert } = require('@openzeppelin/test-helpers');
const ERC20SnapshotMock = artifacts.require('ERC20SnapshotEveryBlockMock');

const { expect } = require('chai');
Expand All @@ -7,6 +7,21 @@ function send (method, params = []) {
return new Promise(resolve => web3.currentProvider.send({ jsonrpc: '2.0', method, params }, resolve));
}

async function batchInBlock (txs) {
const before = await web3.eth.getBlockNumber();

await send('evm_setAutomine', [false]);
const promises = Promise.all(txs.map(fn => fn()));
await send('evm_setIntervalMining', [1000]);
const receipts = await promises;
await send('evm_setAutomine', [true]);
await send('evm_setIntervalMining', [false]);

expect(await web3.eth.getBlockNumber()).to.be.equal(before + 1);

return receipts;
}

contract('ERC20SnapshotEveryBlock', function (accounts) {
const [ initialHolder, recipient, other ] = accounts;

Expand All @@ -18,118 +33,174 @@ contract('ERC20SnapshotEveryBlock', function (accounts) {
beforeEach(async function () {
time.advanceBlock();
this.token = await ERC20SnapshotMock.new(name, symbol, initialHolder, initialSupply);
this.initialBlock = await web3.eth.getBlockNumber();
});

describe('totalSupplyAt', function () {
context('with initial snapshot', function () {
context('with no supply changes after the snapshot', function () {
it('returns the current total supply', async function () {
const blockNumber = await web3.eth.getBlockNumber();
expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply);
expect(await this.token.totalSupplyAt(0)).to.be.bignumber.equal('0');
expect(await this.token.totalSupplyAt(blockNumber - 1)).to.be.bignumber.equal('0');
expect(await this.token.totalSupplyAt(blockNumber)).to.be.bignumber.equal(initialSupply);
});
it('reverts with a snapshot id of 0', async function () {
await expectRevert(this.token.totalSupplyAt(0), 'ERC20Snapshot: id is 0');
});

context('with no supply changes after the snapshot', function () {
it('snapshot before initial supply', async function () {
expect(await this.token.totalSupplyAt(this.initialBlock)).to.be.bignumber.equal('0');
});

it('snapshot after initial supply', async function () {
await time.advanceBlock();
expect(await this.token.totalSupplyAt(this.initialBlock + 1)).to.be.bignumber.equal(initialSupply);
});
});

context('with supply changes', function () {
beforeEach(async function () {
await batchInBlock([
() => this.token.mint(other, new BN('50')),
() => this.token.burn(initialHolder, new BN('20')),
]);
this.operationBlockNumber = await web3.eth.getBlockNumber();
});

context('with supply changes', function () {
beforeEach(async function () {
this.blockNumberBefore = await web3.eth.getBlockNumber();
await this.token.mint(other, new BN('50'));
await this.token.burn(initialHolder, new BN('20'));
this.blockNumberAfter = await web3.eth.getBlockNumber();
});

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

it('snapshots return the supply after the changes', async function () {
expect(await this.token.totalSupplyAt(this.blockNumberAfter)).to.be.bignumber.equal(
await this.token.totalSupply(),
);
});
it('returns the total supply before the changes', async function () {
expect(await this.token.totalSupplyAt(this.operationBlockNumber))
.to.be.bignumber.equal(initialSupply);
});

it('snapshots return the supply after the changes', async function () {
await time.advanceBlock();
expect(await this.token.totalSupplyAt(this.operationBlockNumber + 1))
.to.be.bignumber.equal(initialSupply.addn(50).subn(20));
});
});

describe('with multiple operations over multiple blocks', function () {
beforeEach(async function () {
this.snapshots = [ this.initialBlock ];
await this.token.mint(other, new BN('50'));
this.snapshots.push(await web3.eth.getBlockNumber());
await this.token.transfer(recipient, new BN('10'), { from: initialHolder });
this.snapshots.push(await web3.eth.getBlockNumber());
await this.token.burn(initialHolder, new BN('20'));
this.snapshots.push(await web3.eth.getBlockNumber());
await time.advanceBlock();
this.snapshots.push(await web3.eth.getBlockNumber());
});

it('check snapshots', async function () {
expect(await this.token.totalSupplyAt(this.snapshots[0]))
.to.be.bignumber.equal('0');
// initial mint
expect(await this.token.totalSupplyAt(this.snapshots[1]))
.to.be.bignumber.equal(initialSupply);
// mint 50
expect(await this.token.totalSupplyAt(this.snapshots[2]))
.to.be.bignumber.equal(initialSupply.addn(50));
// transfer: no change
expect(await this.token.totalSupplyAt(this.snapshots[3]))
.to.be.bignumber.equal(initialSupply.addn(50));
// burn 20
expect(await this.token.totalSupplyAt(this.snapshots[4]))
.to.be.bignumber.equal(initialSupply.addn(50).subn(20));
});
});
});

describe('balanceOfAt', function () {
context('with initial snapshot', function () {
context('with no balance changes after the snapshot', function () {
it('returns the current balance for all accounts', async function () {
const blockNumber = await web3.eth.getBlockNumber();
expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(initialSupply);
expect(await this.token.balanceOfAt(initialHolder, 0)).to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(initialHolder, blockNumber - 1)).to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(initialHolder, blockNumber)).to.be.bignumber.equal(initialSupply);
});
it('reverts with a snapshot id of 0', async function () {
await expectRevert(this.token.balanceOfAt(initialHolder, 0), 'ERC20Snapshot: id is 0');
});

context('with no supply changes after the snapshot', function () {
it('snapshot before initial supply', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.initialBlock))
.to.be.bignumber.equal('0');
});

it('snapshot after initial supply', async function () {
await time.advanceBlock();
expect(await this.token.balanceOfAt(initialHolder, this.initialBlock + 1))
.to.be.bignumber.equal(initialSupply);
});
});

context('with balance changes', function () {
beforeEach(async function () {
await batchInBlock([
() => this.token.transfer(recipient, new BN('10'), { from: initialHolder }),
() => this.token.mint(other, new BN('50')),
() => this.token.burn(initialHolder, new BN('20')),
]);
this.operationBlockNumber = await web3.eth.getBlockNumber();
});

it('returns the balances before the changes', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.operationBlockNumber))
.to.be.bignumber.equal(initialSupply);
expect(await this.token.balanceOfAt(recipient, this.operationBlockNumber))
.to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(other, this.operationBlockNumber))
.to.be.bignumber.equal('0');
});

it('snapshots return the balances after the changes', async function () {
await time.advanceBlock();
expect(await this.token.balanceOfAt(initialHolder, this.operationBlockNumber + 1))
.to.be.bignumber.equal(initialSupply.subn(10).subn(20));
expect(await this.token.balanceOfAt(recipient, this.operationBlockNumber + 1))
.to.be.bignumber.equal('10');
expect(await this.token.balanceOfAt(other, this.operationBlockNumber + 1))
.to.be.bignumber.equal('50');
});
});

context('with balance changes', function () {
beforeEach(async function () {
this.blockNumberBefore = await web3.eth.getBlockNumber();
await this.token.transfer(recipient, new BN('10'), { from: initialHolder });
await this.token.mint(other, new BN('50'));
await this.token.burn(initialHolder, new BN('20'));
this.blockNumberAfter = await web3.eth.getBlockNumber();
});

it('returns the balances before the changes', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.blockNumberBefore))
.to.be.bignumber.equal(initialSupply);
expect(await this.token.balanceOfAt(recipient, this.blockNumberBefore))
.to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(other, this.blockNumberBefore))
.to.be.bignumber.equal('0');
});

it('snapshots return the balances after the changes', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.blockNumberAfter))
.to.be.bignumber.equal(await this.token.balanceOf(initialHolder));
expect(await this.token.balanceOfAt(recipient, this.blockNumberAfter))
.to.be.bignumber.equal(await this.token.balanceOf(recipient));
expect(await this.token.balanceOfAt(other, this.blockNumberAfter))
.to.be.bignumber.equal(await this.token.balanceOf(other));
});
describe('with multiple operations over multiple blocks', function () {
beforeEach(async function () {
this.snapshots = [ this.initialBlock ];
await this.token.mint(other, new BN('50'));
this.snapshots.push(await web3.eth.getBlockNumber());
await this.token.transfer(recipient, new BN('10'), { from: initialHolder });
this.snapshots.push(await web3.eth.getBlockNumber());
await this.token.burn(initialHolder, new BN('20'));
this.snapshots.push(await web3.eth.getBlockNumber());
await time.advanceBlock();
this.snapshots.push(await web3.eth.getBlockNumber());
});

context('with multiple transfers in the same block', function () {
beforeEach(async function () {
this.blockNumberBefore = await web3.eth.getBlockNumber();

await send('evm_setAutomine', [false]);
const txs = Promise.all([
this.token.transfer(recipient, new BN('10'), { from: initialHolder }),
this.token.mint(other, new BN('50')),
this.token.burn(initialHolder, new BN('20')),
]);

await send('evm_setIntervalMining', [1000]);
await txs;
await send('evm_setAutomine', [true]);
await send('evm_setIntervalMining', [false]);

this.blockNumberAfter = await web3.eth.getBlockNumber();
expect(this.blockNumberAfter).to.be.equal(this.blockNumberBefore + 1);
});

it('returns the balances before the changes', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.blockNumberBefore))
.to.be.bignumber.equal(initialSupply);
expect(await this.token.balanceOfAt(recipient, this.blockNumberBefore))
.to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(other, this.blockNumberBefore))
.to.be.bignumber.equal('0');
});

it('snapshots return the balances after the changes', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.blockNumberAfter))
.to.be.bignumber.equal(await this.token.balanceOf(initialHolder));
expect(await this.token.balanceOfAt(recipient, this.blockNumberAfter))
.to.be.bignumber.equal(await this.token.balanceOf(recipient));
expect(await this.token.balanceOfAt(other, this.blockNumberAfter))
.to.be.bignumber.equal(await this.token.balanceOf(other));
});
it('check snapshots', async function () {
expect(await this.token.balanceOfAt(initialHolder, this.snapshots[0]))
.to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(recipient, this.snapshots[0]))
.to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(other, this.snapshots[0]))
.to.be.bignumber.equal('0');
// initial mint
expect(await this.token.balanceOfAt(initialHolder, this.snapshots[1]))
.to.be.bignumber.equal(initialSupply);
expect(await this.token.balanceOfAt(recipient, this.snapshots[1]))
.to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(other, this.snapshots[1]))
.to.be.bignumber.equal('0');
// mint 50
expect(await this.token.balanceOfAt(initialHolder, this.snapshots[2]))
.to.be.bignumber.equal(initialSupply);
expect(await this.token.balanceOfAt(recipient, this.snapshots[2]))
.to.be.bignumber.equal('0');
expect(await this.token.balanceOfAt(other, this.snapshots[2]))
.to.be.bignumber.equal('50');
// transfer
expect(await this.token.balanceOfAt(initialHolder, this.snapshots[3]))
.to.be.bignumber.equal(initialSupply.subn(10));
expect(await this.token.balanceOfAt(recipient, this.snapshots[3]))
.to.be.bignumber.equal('10');
expect(await this.token.balanceOfAt(other, this.snapshots[3]))
.to.be.bignumber.equal('50');
// burn 20
expect(await this.token.balanceOfAt(initialHolder, this.snapshots[4]))
.to.be.bignumber.equal(initialSupply.subn(10).subn(20));
expect(await this.token.balanceOfAt(recipient, this.snapshots[4]))
.to.be.bignumber.equal('10');
expect(await this.token.balanceOfAt(other, this.snapshots[4]))
.to.be.bignumber.equal('50');
});
});
});
Expand Down

0 comments on commit 512324a

Please sign in to comment.