Repository: Synthetixio/synthetix-v3
Pull request #794 (2022-02-02 15:23:47)

CP-ERC20HistoricalBalance

codecov[bot]: # [Codecov](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio) Report
> Merging [#794](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio) (9d37cb9) into [main](https://codecov.io/gh/Synthetixio/synthetix-v3/commit/523ea15789947014027493bce732ae4f7ed8315f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio) (523ea15) will **increase** coverage by `2.13%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/graphs/tree.svg?width=650&height=150&src=pr&token=B9BK0U5KAT&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio)](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio)

```diff
@@            Coverage Diff             @@
##             main     #794      +/-   ##
==========================================
+ Coverage   97.12%   99.25%   +2.13%     
==========================================
  Files          70       19      -51     
  Lines        1216      269     -947     
  Branches       54        0      -54     
==========================================
- Hits         1181      267     -914     
+ Misses         35        2      -33     
```

| Flag | Coverage Δ | |
|---|---|---|
| core-contracts | `?` | |
| core-js | `99.25% <100.00%> (+0.02%)` | :arrow_up: |
| core-modules | `?` | |
| deployer | `?` | |
| synthetix-main | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio) | Coverage Δ | |
|---|---|---|
| [packages/core-js/utils/hardhat/rpc.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvY29yZS1qcy91dGlscy9oYXJkaGF0L3JwYy5qcw==) | `100.00% <100.00%> (ø)` | |
| [packages/core-contracts/contracts/token/ERC20.sol](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvY29yZS1jb250cmFjdHMvY29udHJhY3RzL3Rva2VuL0VSQzIwLnNvbA==) | | |
| [packages/deployer/subtasks/deploy-modules.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvc3VidGFza3MvZGVwbG95LW1vZHVsZXMuanM=) | | |
| [...s/deployer/internal/initializable-ast-validator.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvaW50ZXJuYWwvaW5pdGlhbGl6YWJsZS1hc3QtdmFsaWRhdG9yLmpz) | | |
| [packages/core-contracts/contracts/token/ERC721.sol](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvY29yZS1jb250cmFjdHMvY29udHJhY3RzL3Rva2VuL0VSQzcyMS5zb2w=) | | |
| [packages/deployer/internal/router-helper.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvaW50ZXJuYWwvcm91dGVyLWhlbHBlci5qcw==) | | |
| [...kages/deployer/internal/router-source-validator.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvaW50ZXJuYWwvcm91dGVyLXNvdXJjZS12YWxpZGF0b3IuanM=) | | |
| [...kages/deployer/subtasks/validate-initializables.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvc3VidGFza3MvdmFsaWRhdGUtaW5pdGlhbGl6YWJsZXMuanM=) | | |
| [packages/deployer/internal/autosave-object.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvaW50ZXJuYWwvYXV0b3NhdmUtb2JqZWN0Lmpz) | | |
| [packages/deployer/subtasks/finalize-deployment.js](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio#diff-cGFja2FnZXMvZGVwbG95ZXIvc3VidGFza3MvZmluYWxpemUtZGVwbG95bWVudC5qcw==) | | |
| ... and [42 more](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio). Last update [523ea15...9d37cb9](https://codecov.io/gh/Synthetixio/synthetix-v3/pull/794?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Synthetixio).


Patch:
diff --git a/packages/core-contracts/contracts/interfaces/IERC20HistoricalBalance.sol b/packages/core-contracts/contracts/interfaces/IERC20HistoricalBalance.sol
new file mode 100644
index 00000000..a86754fa
--- /dev/null
+++ b/packages/core-contracts/contracts/interfaces/IERC20HistoricalBalance.sol
@@ -0,0 +1,8 @@
+//SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+interface IERC20HistoricalBalance {
+    function totalSupplyAt(uint blockNumber) external view returns (uint);
+
+    function balanceOfAt(address owner, uint blockNumber) external view returns (uint);
+}
diff --git a/packages/core-contracts/contracts/mocks/token/ERC20HistoricalBalanceMock.sol b/packages/core-contracts/contracts/mocks/token/ERC20HistoricalBalanceMock.sol
new file mode 100644
index 00000000..6e9d0780
--- /dev/null
+++ b/packages/core-contracts/contracts/mocks/token/ERC20HistoricalBalanceMock.sol
@@ -0,0 +1,22 @@
+//SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import "../../token/ERC20HistoricalBalance.sol";
+
+contract ERC20HistoricalBalanceMock is ERC20HistoricalBalance {
+    function initialize(
+        string memory tokenName,
+        string memory tokenSymbol,
+        uint8 tokenDecimals
+    ) public {
+        _initialize(tokenName, tokenSymbol, tokenDecimals);
+    }
+
+    function mint(uint256 amount) external {
+        _mint(msg.sender, amount);
+    }
+
+    function burn(uint256 amount) external {
+        _burn(msg.sender, amount);
+    }
+}
diff --git a/packages/core-contracts/contracts/token/ERC20.sol b/packages/core-contracts/contracts/token/ERC20.sol
index 06c14e97..4902d86a 100644
--- a/packages/core-contracts/contracts/token/ERC20.sol
+++ b/packages/core-contracts/contracts/token/ERC20.sol
@@ -97,7 +97,7 @@ contract ERC20 is IERC20, ERC20Storage {
         address from,
         address to,
         uint256 amount
-    ) private {
+    ) internal virtual {
         ERC20Store storage store = _erc20Store();
 
         uint256 accountBalance = store.balanceOf[from];
@@ -117,7 +117,7 @@ contract ERC20 is IERC20, ERC20Storage {
         emit Transfer(from, to, amount);
     }
 
-    function _mint(address to, uint256 amount) internal {
+    function _mint(address to, uint256 amount) internal virtual {
         ERC20Store storage store = _erc20Store();
 
         store.totalSupply += amount;
@@ -130,7 +130,7 @@ contract ERC20 is IERC20, ERC20Storage {
         emit Transfer(address(0), to, amount);
     }
 
-    function _burn(address from, uint256 amount) internal {
+    function _burn(address from, uint256 amount) internal virtual {
         ERC20Store storage store = _erc20Store();
 
         uint256 accountBalance = store.balanceOf[from];
diff --git a/packages/core-contracts/contracts/token/ERC20HistoricalBalance.sol b/packages/core-contracts/contracts/token/ERC20HistoricalBalance.sol
new file mode 100644
index 00000000..5929e41f
--- /dev/null
+++ b/packages/core-contracts/contracts/token/ERC20HistoricalBalance.sol
@@ -0,0 +1,135 @@
+//SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+import "../interfaces/IERC20HistoricalBalance.sol";
+import "./ERC20.sol";
+import "./ERC20HistoricalBalanceStorage.sol";
+
+/*
+    Reference implementations:
+    * OpenZeppelin - https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC20Votes.sol
+*/
+
+contract ERC20HistoricalBalance is ERC20, IERC20HistoricalBalance, ERC20HistoricalBalanceStorage {
+    error InvalidUint32Number(uint blockNumber);
+    error BlockNumberNotYetMined(uint blockNumber);
+
+    function totalSupplyAt(uint blockNumber) external view override returns (uint) {
+        if (blockNumber >= block.number) {
+            revert BlockNumberNotYetMined(blockNumber);
+        }
+
+        return _checkpointsLookup(_erc20HistoricalBalanceStore().totalSupplyCheckpoints, blockNumber);
+    }
+
+    function balanceOfAt(address owner, uint blockNumber) public view override returns (uint) {
+        if (blockNumber >= block.number) {
+            revert BlockNumberNotYetMined(blockNumber);
+        }
+
+        return _checkpointsLookup(_erc20HistoricalBalanceStore().checkpoints[owner], blockNumber);
+    }
+
+    function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) {
+        // We run a binary search to look for the earliest checkpoint taken after `blockNumber`.
+        //
+        // During the loop, the index of the wanted checkpoint remains in the range [low-1, high).
+        // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant.
+        // - If the middle checkpoint is after `blockNumber`, we look in [low, mid)
+        // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high)
+        // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not
+        // out of bounds (in which case we're looking too far in the past and the result is 0).
+        // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is
+        // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out
+        // the same.
+        uint256 high = ckpts.length;
+        uint256 low = 0;
+        while (low < high) {
+            uint256 mid = _average(low, high);
+            if (ckpts[mid].fromBlock > blockNumber) {
+                high = mid;
+            } else {
+                low = mid + 1;
+            }
+        }
+
+        return high == 0 ? 0 : ckpts[high - 1].balance;
+    }
+
+    function _transfer(
+        address from,
+        address to,
+        uint256 amount
+    ) internal override {
+        super._transfer(from, to, amount);
+
+        _moveBalance(from, to, amount);
+    }
+
+    function _mint(address to, uint256 amount) internal override {
+        super._mint(to, amount);
+
+        _moveBalance(address(0), to, amount);
+        _writeCheckpoint(_erc20HistoricalBalanceStore().totalSupplyCheckpoints, _add, amount);
+    }
+
+    function _burn(address from, uint256 amount) internal override {
+        super._burn(from, amount);
+
+        _moveBalance(from, address(0), amount);
+        _writeCheckpoint(_erc20HistoricalBalanceStore().totalSupplyCheckpoints, _subtract, amount);
+    }
+
+    function _moveBalance(
+        address src,
+        address dst,
+        uint256 amount
+    ) private {
+        if (src != dst && amount > 0) {
+            if (src != address(0)) {
+                _writeCheckpoint(_erc20HistoricalBalanceStore().checkpoints[src], _subtract, amount);
+            }
+
+            if (dst != address(0)) {
+                _writeCheckpoint(_erc20HistoricalBalanceStore().checkpoints[dst], _add, amount);
+            }
+        }
+    }
+
+    function _writeCheckpoint(
+        Checkpoint[] storage ckpts,
+        function(uint256, uint256) view returns (uint256) op,
+        uint256 delta
+    ) private {
+        uint256 pos = ckpts.length;
+        uint256 oldWeight = pos == 0 ? 0 : ckpts[pos - 1].balance;
+        uint256 newWeight = op(oldWeight, delta);
+
+        if (pos > 0 && ckpts[pos - 1].fromBlock == block.number) {
+            ckpts[pos - 1].balance = newWeight;
+        } else {
+            ckpts.push(Checkpoint({fromBlock: _safeCastToUint32(block.number), balance: newWeight}));
+        }
+    }
+
+    function _add(uint256 a, uint256 b) private pure returns (uint256) {
+        return a + b;
+    }
+
+    function _subtract(uint256 a, uint256 b) private pure returns (uint256) {
+        return a - b;
+    }
+
+    function _average(uint256 a, uint256 b) internal pure returns (uint256) {
+        // (a + b) / 2 can overflow.
+        return (a & b) + (a ^ b) / 2;
+    }
+
+    function _safeCastToUint32(uint256 number) internal returns (uint32) {
+        if (block.number > type(uint32).max) {
+            revert InvalidUint32Number(block.number);
+        }
+
+        return uint32(number);
+    }
+}
diff --git a/packages/core-contracts/contracts/token/ERC20HistoricalBalanceStorage.sol b/packages/core-contracts/contracts/token/ERC20HistoricalBalanceStorage.sol
new file mode 100644
index 00000000..ad543c81
--- /dev/null
+++ b/packages/core-contracts/contracts/token/ERC20HistoricalBalanceStorage.sol
@@ -0,0 +1,21 @@
+//SPDX-License-Identifier: MIT
+pragma solidity ^0.8.0;
+
+contract ERC20HistoricalBalanceStorage {
+    struct Checkpoint {
+        uint32 fromBlock;
+        uint256 balance;
+    }
+
+    struct ERC20HistoricalBalanceStore {
+        mapping(address => Checkpoint[]) checkpoints;
+        Checkpoint[] totalSupplyCheckpoints;
+    }
+
+    function _erc20HistoricalBalanceStore() internal pure returns (ERC20HistoricalBalanceStore storage store) {
+        assembly {
+            // bytes32(uint(keccak256("io.synthetix.ERC20HistoricalBalance")) - 1)
+            store.slot := 0x393eda35c1f9f2ed61beadc91c5bc71460000d96e49875c3538eefc5de97d456
+        }
+    }
+}
diff --git a/packages/core-js/utils/hardhat/rpc.js b/packages/core-js/utils/hardhat/rpc.js
index 583670dc..c25df4ff 100644
--- a/packages/core-js/utils/hardhat/rpc.js
+++ b/packages/core-js/utils/hardhat/rpc.js
@@ -18,6 +18,32 @@ async function fastForward(seconds, provider) {
   await mineBlock(provider);
 }
 
+async function fastForwardTo(time, provider) {
+  const now = await getTime(provider);
+
+  if (time < now) {
+    throw 'Cannot fast forward to a past date.';
+  }
+
+  await fastForward(time - now, provider);
+}
+
+async function getTime(provider) {
+  const block = await provider.getBlock();
+
+  return block.timestamp;
+}
+
+async function getBlock(provider) {
+  const block = await provider.getBlock();
+
+  return block.number;
+}
+
+async function advanceBlock(provider) {
+  await mineBlock(provider);
+}
+
 async function mineBlock(provider) {
   await provider.send('evm_mine');
 }
@@ -26,4 +52,8 @@ module.exports = {
   takeSnapshot,
   restoreSnapshot,
   fastForward,
+  fastForwardTo,
+  advanceBlock,
+  getBlock,
+  getTime,
 };


Test patch:
diff --git a/packages/core-contracts/test/contracts/token/ERC20HistoricalBalance.test.js b/packages/core-contracts/test/contracts/token/ERC20HistoricalBalance.test.js
new file mode 100644
index 00000000..d3479cca
--- /dev/null
+++ b/packages/core-contracts/test/contracts/token/ERC20HistoricalBalance.test.js
@@ -0,0 +1,298 @@
+const { ethers } = hre;
+const { advanceBlock, getBlock } = require('@synthetixio/core-js/utils/hardhat/rpc');
+const assertRevert = require('@synthetixio/core-js/utils/assertions/assert-revert');
+const assertBn = require('@synthetixio/core-js/utils/assertions/assert-bignumber');
+
+describe('ERC20HistoricalBalance', () => {
+  let ERC20;
+
+  let user1, user2;
+
+  before('identify signers', async () => {
+    [user1, user2] = await ethers.getSigners();
+  });
+
+  before('deploy the contract', async () => {
+    const factory = await ethers.getContractFactory('ERC20HistoricalBalanceMock');
+    ERC20 = await factory.deploy();
+    const tx = await ERC20.initialize('Synthetix Network Token', 'snx', 18);
+    await tx.wait();
+  });
+
+  describe('before minting any tokens', () => {
+    before('set first block', async function () {
+      await advanceBlock(ethers.provider);
+    });
+
+    it('the total supply is 0', async () => {
+      assertBn.equal(await ERC20.totalSupply(), 0);
+    });
+
+    it('the total supply was 0', async () => {
+      const currentBlock = await getBlock(ethers.provider);
+      assertBn.equal(await ERC20.totalSupplyAt(currentBlock - 1), 0);
+    });
+  });
+
+  describe('when tokens are minted', () => {
+    let totalSupply = ethers.BigNumber.from('0');
+    let receipt;
+
+    before('initial mint for the users', async () => {
+      let tx;
+
+      tx = await ERC20.connect(user1).mint(100);
+      receipt = await tx.wait();
+      totalSupply = totalSupply.add(100);
+
+      tx = await ERC20.connect(user2).mint(100);
+      receipt = await tx.wait();
+      totalSupply = totalSupply.add(100);
+    });
+
+    it('reads the right values', async () => {
+      await advanceBlock(ethers.provider);
+
+      assertBn.equal(await ERC20.totalSupplyAt(receipt.blockNumber), totalSupply);
+      assertBn.equal(await ERC20.balanceOfAt(user1.address, receipt.blockNumber), 100);
+      assertBn.equal(await ERC20.balanceOfAt(user2.address, receipt.blockNumber), 100);
+    });
+
+    describe('when getting the past values', () => {
+      describe('when minting', () => {
+        const initialBalances = [];
+        let initialTotalSupply;
+        let r1, r2, r3;
+
+        before('read initial balances', async () => {
+          initialBalances.push(await ERC20.balanceOf(user1.address));
+          initialTotalSupply = await ERC20.totalSupply();
+        });
+
+        before('do some transactions', async () => {
+          let tx;
+          tx = await ERC20.connect(user1).mint(10);
+          r1 = await tx.wait();
+
+          tx = await ERC20.connect(user1).mint(20);
+          r2 = await tx.wait();
+
+          tx = await ERC20.connect(user1).mint(30);
+          r3 = await tx.wait();
+        });
+
+        it('reads the right values', async () => {
+          await advanceBlock(ethers.provider);
+
+          assertBn.equal(await ERC20.totalSupplyAt(r1.blockNumber), initialTotalSupply.add(10));
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r1.blockNumber),
+            initialBalances[0].add(10)
+          );
+
+          assertBn.equal(await ERC20.totalSupplyAt(r2.blockNumber), initialTotalSupply.add(30));
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r2.blockNumber),
+            initialBalances[0].add(30)
+          );
+
+          assertBn.equal(await ERC20.totalSupplyAt(r3.blockNumber), initialTotalSupply.add(60));
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r3.blockNumber),
+            initialBalances[0].add(60)
+          );
+        });
+      });
+
+      describe('when burning', () => {
+        const initialBalances = [];
+        let initialTotalSupply;
+        let r1, r2, r3;
+
+        before('read initial balances', async () => {
+          initialBalances.push(await ERC20.balanceOf(user1.address));
+          initialTotalSupply = await ERC20.totalSupply();
+        });
+
+        before('do some transactions', async () => {
+          let tx;
+          tx = await ERC20.connect(user1).burn(10);
+          r1 = await tx.wait();
+
+          tx = await ERC20.connect(user1).burn(20);
+          r2 = await tx.wait();
+
+          tx = await ERC20.connect(user1).burn(30);
+          r3 = await tx.wait();
+        });
+
+        it('reads the right values', async () => {
+          await advanceBlock(ethers.provider);
+
+          assertBn.equal(await ERC20.totalSupplyAt(r1.blockNumber), initialTotalSupply.sub(10));
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r1.blockNumber),
+            initialBalances[0].sub(10)
+          );
+
+          assertBn.equal(await ERC20.totalSupplyAt(r2.blockNumber), initialTotalSupply.sub(30));
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r2.blockNumber),
+            initialBalances[0].sub(30)
+          );
+
+          assertBn.equal(await ERC20.totalSupplyAt(r3.blockNumber), initialTotalSupply.sub(60));
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r3.blockNumber),
+            initialBalances[0].sub(60)
+          );
+        });
+      });
+
+      describe('when transfering', () => {
+        const initialBalances = [];
+        let initialTotalSupply;
+        let r1, r2, r3;
+
+        before('read initial balances', async () => {
+          initialBalances.push(await ERC20.balanceOf(user1.address));
+          initialBalances.push(await ERC20.balanceOf(user2.address));
+          initialTotalSupply = await ERC20.totalSupply();
+        });
+
+        before('do some transactions', async () => {
+          let tx;
+          tx = await ERC20.connect(user2).transfer(user1.address, 10);
+          r1 = await tx.wait();
+
+          tx = await ERC20.connect(user2).transfer(user1.address, 20);
+          r2 = await tx.wait();
+
+          tx = await ERC20.connect(user2).transfer(user1.address, 30);
+          r3 = await tx.wait();
+        });
+
+        it('reads the right values', async () => {
+          await advanceBlock(ethers.provider);
+
+          assertBn.equal(await ERC20.totalSupplyAt(r1.blockNumber), initialTotalSupply);
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r1.blockNumber),
+            initialBalances[0].add(10)
+          );
+          assertBn.equal(
+            await ERC20.balanceOfAt(user2.address, r1.blockNumber),
+            initialBalances[1].sub(10)
+          );
+
+          assertBn.equal(await ERC20.totalSupplyAt(r2.blockNumber), initialTotalSupply);
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r2.blockNumber),
+            initialBalances[0].add(30)
+          );
+          assertBn.equal(
+            await ERC20.balanceOfAt(user2.address, r2.blockNumber),
+            initialBalances[1].sub(30)
+          );
+
+          assertBn.equal(await ERC20.totalSupplyAt(r3.blockNumber), initialTotalSupply);
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r3.blockNumber),
+            initialBalances[0].add(60)
+          );
+          assertBn.equal(
+            await ERC20.balanceOfAt(user2.address, r3.blockNumber),
+            initialBalances[1].sub(60)
+          );
+        });
+      });
+
+      describe('when looking for an intermediate blockNumber', () => {
+        const initialBalances = [];
+        let initialTotalSupply, intermediateTotalSupply;
+        let r1, r2, ri, rn;
+
+        before('read initial balances', async () => {
+          initialBalances.push(await ERC20.balanceOf(user1.address));
+          initialBalances.push(await ERC20.balanceOf(user2.address));
+          initialTotalSupply = await ERC20.totalSupply();
+          intermediateTotalSupply = initialTotalSupply;
+        });
+
+        before('do some transactions', async () => {
+          let tx;
+          tx = await ERC20.connect(user1).mint(10);
+          r1 = await tx.wait();
+          intermediateTotalSupply = intermediateTotalSupply.add(10);
+
+          tx = await ERC20.connect(user1).mint(20);
+          r2 = await tx.wait();
+          intermediateTotalSupply = intermediateTotalSupply.add(20);
+
+          // Do lot of transactions in between for another account and pick one
+          for (let i = 0; i < 100; i++) {
+            tx = await ERC20.connect(user2).mint(30);
+            const rx = await tx.wait();
+            if (i <= 42) {
+              intermediateTotalSupply = intermediateTotalSupply.add(30);
+              if (i == 42) {
+                ri = rx;
+              }
+            }
+          }
+
+          tx = await ERC20.connect(user1).mint(30);
+          rn = await tx.wait();
+        });
+
+        it('reads the right values', async () => {
+          await advanceBlock(ethers.provider);
+
+          assertBn.equal(await ERC20.totalSupplyAt(r1.blockNumber), initialTotalSupply.add(10));
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r1.blockNumber),
+            initialBalances[0].add(10)
+          );
+
+          assertBn.equal(await ERC20.totalSupplyAt(r2.blockNumber), initialTotalSupply.add(30));
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, r2.blockNumber),
+            initialBalances[0].add(30)
+          );
+
+          // Should be the same as r2 for user1 and the point in time for totalSupply
+          assertBn.equal(await ERC20.totalSupplyAt(ri.blockNumber), intermediateTotalSupply);
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, ri.blockNumber),
+            initialBalances[0].add(30)
+          );
+
+          assertBn.equal(
+            await ERC20.balanceOfAt(user1.address, rn.blockNumber),
+            initialBalances[0].add(60)
+          );
+        });
+      });
+    });
+
+    describe('when attempting to get the value from a block not yet mined', () => {
+      it('reverts totalSupplyAt()', async () => {
+        const blockNumber = await getBlock(ethers.provider);
+
+        await assertRevert(
+          ERC20.totalSupplyAt(blockNumber + 1),
+          `BlockNumberNotYetMined(${blockNumber + 1})`
+        );
+      });
+
+      it('reverts balanceOfAt()', async () => {
+        const blockNumber = await getBlock(ethers.provider);
+
+        await assertRevert(
+          ERC20.balanceOfAt(user1.address, blockNumber + 1),
+          `BlockNumberNotYetMined(${blockNumber + 1})`
+        );
+      });
+    });
+  });
+});
diff --git a/packages/core-js/test/utils/hardhat/rpc.test.js b/packages/core-js/test/utils/hardhat/rpc.test.js
index 225b4ba7..dcc2dff4 100644
--- a/packages/core-js/test/utils/hardhat/rpc.test.js
+++ b/packages/core-js/test/utils/hardhat/rpc.test.js
@@ -1,12 +1,22 @@
 const assert = require('assert/strict');
 const sinon = require('sinon');
-const { takeSnapshot, restoreSnapshot, fastForward } = require('../../../utils/hardhat/rpc');
+const {
+  takeSnapshot,
+  restoreSnapshot,
+  advanceBlock,
+  fastForward,
+  fastForwardTo,
+  getBlock,
+  getTime,
+} = require('../../../utils/hardhat/rpc');
 
 const fakeProvider = {
-  send() {
-    return new Promise((resolve) => {
-      resolve(42);
-    });
+  async send() {
+    return 42;
+  },
+
+  async getBlock() {
+    return { timestamp: 1337, number: 42 };
   },
 };
 
@@ -84,4 +94,69 @@ describe('utils/hardhat/rpc.js', () => {
       assert.equal(provider.send.getCall(1).args[1], undefined);
     });
   });
+
+  describe('when calling fastForwardTo', () => {
+    describe('to the past', function () {
+      it('throws', async function () {
+        try {
+          await fastForwardTo(1000, provider);
+        } catch (err) {
+          assert.ok(err.toString().includes('Cannot fast forward to a past date'));
+        }
+      });
+    });
+
+    describe('to the future', function () {
+      before('clear spy history', () => {
+        provider.send.resetHistory();
+      });
+
+      before('call fastForward', async () => {
+        await fastForwardTo(10000, provider);
+      });
+
+      it('calls the provider.send twice', () => {
+        assert(provider.send.calledTwice);
+      });
+
+      it('calls the provider.send with the right params', () => {
+        assert.equal(provider.send.getCall(0).args[0], 'evm_increaseTime');
+        assert.deepEqual(provider.send.getCall(0).args[1], [10000 - 1337]);
+
+        assert.equal(provider.send.getCall(1).args[0], 'evm_mine');
+        assert.equal(provider.send.getCall(1).args[1], undefined);
+      });
+    });
+  });
+
+  describe('when calling advanceBlock', () => {
+    before('clear spy history', () => {
+      provider.send.resetHistory();
+    });
+
+    before('call advanceBlock', async () => {
+      await advanceBlock(provider);
+    });
+
+    it('calls the provider.send once', () => {
+      assert(provider.send.calledOnce);
+    });
+
+    it('calls the provider.send with the right params', () => {
+      assert.equal(provider.send.getCall(0).args[0], 'evm_mine');
+      assert.equal(provider.send.getCall(0).args[1], undefined);
+    });
+  });
+
+  describe('when calling getBlock', () => {
+    it('returns the expected value', async () => {
+      assert.equal(await getBlock(provider), 42);
+    });
+  });
+
+  describe('when calling getTime', () => {
+    it('returns the expected value', async () => {
+      assert.equal(await getTime(provider), 1337);
+    });
+  });
 });