A mechanical watch is a more dependable measure of time than a blockchain timestamp, so long as I keep wearing it.

Testing time-dependent logic in Ethereum Smart Contracts

Dave Sag
Published in
6 min readFeb 26, 2018

--

It’s not uncommon to write a contract in Solidity that only allows certain actions to be performed within specific time constraints. Popular examples of this include the OpenZeppelin TimedCrowdsale contract.

When testing a contract that builds on top of TimedCrowdsale you can run into the situation where the action you are testing (say, distribution of refunds) can only be tested once the Crowdsale has closed.

The contract has the following function:

function hasClosed() public view returns (bool) {
return now > closingTime;
}

This checks the value of now and compares it to the contract’sclosingTime timestamp.

OpenZeppelin also provide a very commonly used FinalizableCrowdsale contract which extends the TimedCrowdsale. Typically to implement this you would override the finalization function, which is invoked from the finalize function as follows:

function finalize() onlyOwner public {
require(!isFinalized);
require(hasClosed());
finalization();
Finalized();
isFinalized = true;
}

To unit test your own finalization function you need to ensure that hasClosed() returns true. This means you need to bend time.

Also read: The Best Hardware wallet

What is time?

Most programming environments have a concept of time. In Javascript, for example, you can always get the current time via const now = Date.now(). This assigns the current Unix Epoch time (the number of milliseconds since January 1, 1970) to the variable now, based on the clock in the computer executing the script.

In an Ethereum smart contract however, time is handled a bit differently. In the Solidity language now is actually a shorthand reference to the current block’s timestamp, that is the number of seconds since the Unix Epoch that the function’s transaction was recorded to the blockchain. That time therefore depends entirely on the clock in the machine mining that particular block.

In general a miner is expected to have an accurate clock. If your clock is out of sync with other miners’ clocks by more than about 12 seconds then you will have trouble connecting to peers. Manipulation of time by miners is something worth guarding against. Logic that depends on highly accurate timestamps is generally discouraged.

The Ethereum Virtual Machine only requires that the timestamp of a block is greater than the referenced previous block’s timestamp. There is a reference in the original Ethereum whitepaper to constraining timestamps to 15 minutes, but that information is out of date. The most recent documentation, the Ethereum ‘Yellow Paper’ has dropped that 15-minute limit.

See Block Header Validity (section 4.4.4, equation 48) in https://github.com/ethereum/yellowpaper

For most purposes, however, if the timestamps vary by even as much as a few minutes, this does not present a major issue.

Testing time.

Truffle allows you to write tests of smart contracts using an adapted version of the standard mochajs unit testing framework. You can write those tests in Javascript and take advantage of all the patterns Mocha provides, such as a before block, and breaking tests down into contexts.

Let’s say I’ve implemented my own finalization function in a contract that extends the FinalizableCrowdsale by adding an overpaymentVault, a special wallet that holds Ether people send that’s over the cap set for an indivudual. We want to allow people to withdraw their overpayments once the crowdsale goes live, so we enhance the finalization function as follows:

function finalization() internal {
overpaymentVault.enableRefunds();
super.finalization();
}

and override the standard claimRefund function:

function claimRefund() public {
require(isFinalized);
if (!goalReached() && vault.deposited(msg.sender) != 0) {
vault.refund(msg.sender);
}
if (overpaymentVault.deposited(msg.sender) != 0) {
overpaymentVault.refund(msg.sender);
}
}

To test claimRefund we need to be able to finalize the contract. Because finalization is an internal function; we can’t test it directly from our unit tests. Thus, in order to test claimRefund we need to first call finalize, and that means the crowdsale needs hasClosed() to return true.

Wind the clock forward.

To wind the clock forward in my tests I wrote a small helper utility I call timeTravel that literally instructs the miner to wind the clock forward by sending a evm_increaseTime instruction to the blockchain. This is not a standard EVM instruction, but is something that’s been added to Ganache, which Truffle uses internally as a test blockchain.

evm_increaseTime : Jump forward in time. Takes one parameter, which is the amount of time to increase in seconds. Returns the total time adjustment, in seconds.

Ganache also provides another non-standard instruction, evm_mine, which simply forces a block to be mined.

In combination those instructions can be sent using web3 to force the blockchain to travel forwards in time.

const jsonrpc = '2.0'const id = 0const send = (method, params = []) =>
web3.currentProvider.send({ id, jsonrpc, method, params })
const timeTravel = async seconds => {
await send('evm_increaseTime', [seconds])
await send('evm_mine')
}
module.exports = timeTravel

I can use this as follows:

contract('MyCrowdsale', accounts => {
const [
wallet,
overpaymentWallet,
punter
] = accounts.slice(1)
let crowdsale
let token
let refunded
openingTime = Math.floor(new Date().getTime() / 1000)
closingTime = openingTime + SECONDS_IN_A_DAY
const rate = toWei(0.5)
const amount = toWei(1.2)
const expectedMax = amount.minus(rate.times(2))
const goal = toWei(1)
before(async () => {
token = await MintableToken.new()
crowdsale = await MyCrowdsale.new(
openingTime,
closingTime,
wallet,
overpaymentWallet,
token.address,
rate,
goal
)
await crowdsale.buyTokens(
punter, { value: amount, from: punter })
await timeTravel(SECONDS_IN_A_DAY * 2)
await crowdsale.finalize()
const balance = web3.eth.getBalance(punter)
await crowdsale.claimRefund({ from: punter })
const newBalance = web3.eth.getBalance(punter)
refunded = newBalance.minus(balance)
})
it('claimRefund refunds the overpayment', () => {
// not exact as gas costs can vary slightly
assert.isTrue(refunded.gt(0) && refunded.lt(expectedMax))
})
})

But you can’t go back!

The time-travelled tests MUST run last. Once you’ve wound the clock forwards you can not wind it back again. This is problematic as you can not predict the order in which your tests will run. Tests do run in a predictable order within a single contract block but once you start breaking your tests up into multiple files, with their own contract blocks, you lose the ability to predict the order. This means your tests are likely to break in unpredictable ways.

Testing time, take two.

It turns out for most contracts that adjusting the clock in your blockchain is overkill. A simpler way is to extend your smart contract with a higher-order mock contract that includes a function such as:

contract MockMyCrowdsale is MyCrowdsale {
function turnBackTime(uint256 secs) external {
openingTime -= secs;
closingTime -= secs;
}
}

Then in the above test, invoke MockMyCrowdsale.new(...) and instead of calling:

await timeTravel(SECONDS_IN_A_DAY * 2)

you can invoke:

await crowdsale.turnBackTime(SECONDS_IN_A_DAY * 2)

Now the test won’t corrupt the blockchain for your other tests.

After much experimentation I am yet to find an example where you actually need to manipulate the blockchain’s own time in preference to just overriding the contract’s own time-specific values.

Conclusion

By leveraging a couple of non-standard EVM instructions provided by the Ganache Ethereum testnet within the Truffle framework it is possible to wind time forwards in order to test time-dependent logic, but this makes your tests very fragile. Instead, it’s better to extend the contract you are testing with a mock-contract that supplies functions you can use to force the internal time variables to hold whatever values you need.

Links

Like this but not a subscriber? You can support the author by joining via davesag.medium.com.

Join Coinmonks Telegram Channel and Youtube Channel get daily Crypto News

Also, Read

--

--