TL;DR: USE THE NPM PACKAGE

There is an excellent Medium article by Andy Watt about testing on the Ethereum blockchain which provides useful insights and helper functions. Given that a contract cannot be overwritten, it is vital that code “execute correctly on the blockchain,” as a bug can cost notable money and resources — see, for instance, The DAO Hack, Parity Wallet Bug.

The following illuminates a bit of experience on writing tests and building off Andy’s findings. In particular, this explores testing time-dependent code along with methods and tooling that may not be widely known. These methods can help the developer community write more robust contracts and more isolated tests, hopefully allowing us to collectively avoid having similar hacks to The DAO and Parity Wallet Bug.

Versions

Truffle v5.0.5 (core: 5.0.5)

Solidity v0.5.0 (solc-js)

Node v11.10.0

Ganache CLI v6.3.0 (ganache-core: 2.4.0)

Setting the Time

The first feature to cover is setting the start time within ganache-cli, which was added in October 2018 and a part of the 6.2.0 release. Take a look:

ganache-cli — help

…

-t, — time Date (ISO 8601) that the first block should start. Use this feature, along with the

evm_increaseTime method to test time-dependent code. [string]

…

Read more about ISO 8061 here.

Example (Set Time)

Start the client with the date set to February 15, 2019 15:53:00 UTC.

$ ganache-cli --time ‘2019-02-15T15:53:00+00:00’

Start the console in another terminal. It will automatically connect with the client.

$ truffle console

truffle(development)> blockNum = await web3.eth.getBlockNumber()

undefined

truffle(development)> blockNum

0

truffle(development)> block = await web3.eth.getBlock(blockNum)

undefined

truffle(development)> block[‘timestamp’]

1550245980

Convert the unix timestamp to a date string.

$ date -u -r “1550245980”

Fri Feb 15 15:53:00 UTC 2019

The date timestamp is February 15, 2019 — the date passed into ganache-cli using the --time flag.

Example (Don’t Set Time)

Start the client in a terminal, but this time don’t set the time.

$ ganache-cli

Start the console in another terminal. It will automatically connect with the client.

$ truffle console

truffle(development)> blockNum = await web3.eth.getBlockNumber()

undefined

truffle(development)> blockNum

0

truffle(development)> block = await web3.eth.getBlock(blockNum)

undefined

truffle(development)> block[‘timestamp’]

1552252405

Convert the unix timestamp to a date string.

$ date -u -r “1552252405”

Sun Mar 10 21:13:25 UTC 2019

The date timestamp is the date the example was created: March 10, 2019.

Example Contract and Tests

By pinning the start block time, it is possible to write tests that exercise functionality before and after a specific time. Below is an example TimeContract along with tests to demonstrate this. The repo is located here.

The first test asserts current time is after Sun Feb 10, 2019 00:00:00 UTC.

The second test asserts current time is after Wed Mar 20, 2019 00:00:00 UTC.

Test Output

Start up the ganache client by setting the current time to Fri Feb 15, 2019 15:53:00 UTC.

$ ganache-cli --time 2019-02-15T15:53:00+00:00

Here is the output of the tests:

$ truffle test

Compiling ./contracts/Migrations.sol…

Compiling ./contracts/TimeContract.sol…

Contract: TimeContract

✓ Sun Feb 10 00:00:00 UTC 2019 (before current time)

✓ Wed Mar 20 00:00:00 UTC 2019 (after current time) 2 passing (181ms)

Here it can be seen that indeed the assertions from above hold true.

Jumping Through Time

The second feature to discuss is jumping through time.

There are JSON RPC API methods within the ganache EVM that allow the simulation of time events:

Methods

+-----------------------------+-----------------------------+

| Action | Ganache EVM Name |

+-----------------------------+-----------------------------+

| Advance the time | evm_increaseTime |

| Advance the block(s) | evm_mine |

| Advance both time and block | evm_increaseTime + evm_mine |

| Save time | evm_snapshot |

| Revert time | evm_revert |

+-----------------------------+-----------------------------+

These methods open up a world of control. Read more about the ones implemented in ganache-cli here.

Take a look at the snippet below that wraps the above methods in testing helper methods:

The snippet is an expansion of Andy Watt’s code from the Medium article mentioned at the beginning of this article with the addition of two new methods.

Jumping Forward

To move forward in time call advanceTimeAndBlock(<time>) or advanceTime(<time>) with the number of seconds (<time>) desired to advance. These methods use JSON RPC evm_increaseTime and evm_mine methods.

For example to move forward 10 days, use the utils.js file in the following way:

//86400 seconds in a day

advancement = 86400 * 10 // 10 Days

await helper.advanceTimeAndBlock(advancement)

Below is the updated test code:

Here, the last test moves the time forward and asserts that ‘now’ is after instance_2’s date.

Here is the test in action:

$ truffle test

Compiling ./contracts/Migrations.sol…

Compiling ./contracts/TimeContract.sol…

Contract: TimeContract

✓ Sun Feb 10 00:00:00 UTC 2019 (before current time) (39ms)

✓ Wed Mar 20 00:00:00 UTC 2019 (after current time)

✓ Wed Mar 20 00:00:00 UTC 2019 (after current time) (48ms) 3 passing (249ms)

It can be seen that the assertions hold true.

However, if the tests are run again, one of the tests fails:

$ truffle test

Compiling ./contracts/Migrations.sol...

Compiling ./contracts/TimeContract.sol...





Contract: TimeContract

✓ Sun Feb 10 00:00:00 UTC 2019 (before current time)

1) Wed Mar 20 00:00:00 UTC 2019 (after current time)

> No events were emitted

✓ Wed Mar 20 00:00:00 UTC 2019 (after current time) (40ms)





2 passing (237ms)

1 failing



1) Contract: TimeContract

Wed Mar 20 00:00:00 UTC 2019 (after current time):



output should have been false

+ expected - actual



-true

+false



at Context.it (TestTimeContract.js:22:16)

at processTicksAndRejections (internal/process/next_tick.js:81:5)

Why is This?

In the previous test run, time was moved forward by 100 days. The local blockchain kept record of the advancement in time and remains at +100 days from our start date. Therefore, the next time the tests are run, they will fail because the second test run’s start date will begin from the prior test run’s end date. In order for these tests to successfully run again, the ganache-cli needs to be restarted using the --time flag with the initial date provided above: 2019-02-15T15:53:00+00:00.

Jumping Backward

When jumping backward, it is necessary to first capture the current time as to know where to jump backwards to. This is what the takeSnapshot() and revertToSnapshot(id) methods do. These methods use JSON RPC evm_snapshot and evm_revert methods.

For example to take a snapshot and capture the ID use the utils.js file in the following way:

snapShot = await helper.takeSnapshot()

snapshotId = snapShot[‘result’]

To revert the snapshot using the captured ID use:

await helper.revertToSnapshot(snapshotId)

Below is the updated test code:

This ensures that before running any test, the ID is saved. After each test, revert is called with the ID to move back to the state prior to running the test.

The output is not different from the output in ‘Jumping Forward’, but the test can be repeatedly run without failure.

In reviewing the output of ganache-cli note the EVM commands as it saves, moves forward, and moves backward in time.

evm_snapshot

Saved snapshot #2

evm_increaseTime

evm_mine

eth_getBlockByNumber

eth_getBlockByNumber

eth_getBlockByNumber

net_version

eth_call

evm_revert

Reverting to snapshot #2

Conclusion

By introducing the functionality to manipulate time, it is possible to test time-dependent code in an isolated manner. Introducing statelessness ensures idempotence among test runs, thereby making test development for the blockchain significantly easier. Hopefully the demonstrations here are useful and empower the community to write safer and more robust contracts. If you have any questions or comments, please share them below; collaboration is welcome and encouraged at Fluidity.

Disclosures

Please be aware that hopping forward and backward in time is not possible on the mainnet; these examples are for testing purposes only.

It is important to note that, regarding jumping backward, Truffle’s framework already does this with their ‘Clean-room environment’ when using “contract() instead of describe().” However, this is on a contract() level and not on a per test level.

It is also worth noting in the above code examples the time granularity is one day and that doing this on a minute level may not work; Ethereum block times are approximate, ranging from 10–19 seconds (can be higher/lower). Both the core Solidity documentation as well as Consensys Diligence recommend not relying on the exact timestamp, “…because not every year equals 365 days and not even every day has 24 hours because of leap seconds” per the Solidity docs.

Code

More about Fluidity