Standing the Time of Test with Truffle and Ganache
Testing Ethereum smart contracts that depend on real-world time is hard. In this tutorial, we take a closer look at building the test infrastructure to support time-sensitive tests for smart contracts using Solidity, Ganache, and Truffle.
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
- Time Contract Example Code
- Ganache Time Traveler Utils Code (Star it!)
- Use the npm package (Use it!)
More about Fluidity
- Subscribe to the Fluidity Blog
- Follow Fluidity on Facebook, Twitter, LinkedIn, and YouTube