Time manipulation in smart contracts

Christian Peters
Solidified
Published in
7 min readSep 21, 2018

Sometimes concepts we took for granted are challenged. Did this happen to time recently?

This article has contributions from Adrian Kuchinke . Thanks for that!

After two frustrating years of trying to land a gig in teaching, a talented but mediocre-graded physicist was able to get a job as a level III assistant examiner at the Bern patent office. Much of his work was related to questions about transmission of electric signals and he therefore followed closely how his field tried to explain propagation of electromagnetic or gravitational forces with more and more odd and complex assumptions.

With a one-in-a-generation insight, he suggested that all the weirdness would fade away if we accept that time is not a constant. It’s relative. This ability of out-of-the-box-thinking is the reason why most of the celebrity names we know casually today will be unknown in hundreds of years to come, while this one will remain: Albert Einstein.

Einstein changed the way we think about time forever. By doing so he buried a proposed concept that was all the rage back then — a concept named ether.

Time in smart contracts

Time is something that we measure after a definition that has emerged from loosely branching the way of the sun at daylight into slots up to today’s definition that about 9 billion oscillations of the caesium atom form a second. This is rather involved to measure on hardware, which is why micro-controllers ship a small crystal that vibrates at a specific rate when electric current is passed through it. If computers are networked, they can sync their time with trusted time servers using the Network Time Protocol to within a few milliseconds of Coordinated Universal Time (UTC).

That sounds like a solid solution, but it’s not of much use within a decentralized network, as we do not trust anyone and can’t therefore come up with an authoritative source for UTC. Therefore we must turn back, to the only authoritative thing we have: We do not trust the origin of a transaction. We do not trust servers within the network. In blocks we trust.

There are a lot of occasions where you want to have time constraints such as deadlines, life spans and expiration times for use cases such as auctions, subscriptions or ICOs.

Here’s a fundamental law that we have to accept:

We will be able to say that time was approximately correct within the span of contract execution after finality is reached.

This quite cumbersome phrasing tries to capture two assumptions:

  1. We can relate to time only within rough intervals and not in the accuracy of seconds or even milliseconds.
  2. While we do have variables relating to time available at runtime, it may be tweaked by the miner and there are incentive mechanisms in place to prevent that. Therefore, as always when we lay trust in blocks, whether the time given by the miner was in the rough interval that is acceptable will only reveal when we are reasonably sure that the block is included in the authoritative chain — aka when a sufficient number of blocks are mined on top.

In general, there are two options to include a time factor into a solidity contract: relating to the current block number or relating to the current timestamp.

block.number

Each block has a number property and this will always be the number of the current block. The integrity is guaranteed by the ethereum protocol and the network won’t accept proposed blocks with this number not being sequential.

Consequently, this property is safe to use. However, block time is not a constant, it is a difficulty-tuning factor. It was originally targeted to be at around 12 seconds, was at 17 seconds for a long time and due to latest protocol updates it is converging to 14 seconds but had some significant outliers as well.

Average block time in the ethereum network (source: etherscan.io)

The takeaway of this is that you can measure time in approximately 14–15 second intervals. However, short intervals are sensitive to small outliers (the next block may be found significantly faster or slower than the average time) while larger intervals are becoming more and more unpredictable regarding network changes. It changed somewhat over the last years, including the drastic increase in the prelude of the Metropolis Byzantium hard fork enforced at 2017/10/15.

block.timestamp

The current timestamp as seconds since the unix epoch (1970/01/01) is given to you as well. This variable comes from the miner and we do not trust the miner. The miner is bound only to increase the timestamp to be higher than the previous block, therefore it may as well be a time far in the future or only a slight increase after the last block. However, miners do have an incentive to accept blocks that clock-in close to the difficulty-target time.

Accepting blocks that are far in the future are a strong signal that a node is not honest, so why wasting resources to validate a block that you don’t trust? And even if a node would validate it, it would have to adjust it’s own system time for future mining, which is a weird thing to do.

Consequently, geth — the reference implementation used by more than 50% of all nodes — will reject blocks which have a higher timestamp than their current system time, leading to very little room for manipulation beyond the current time.

Geth has dedicated errors for block.number and block.timestamp irregularities.

Publishing a block with a shorter time is more reasonable. However, the difficulty of ethereum is intended to work against the networks hashing power, effectively keeping the block time stable. A short blocktime is interpreted as an increase in the overall hashing power, resulting in the difficulty increasing.

This gives published stale blocks (uncles in ethereum) the chance to be prioritized over the malicious one: Since other miners have no reason to build upon a block that skyrockets the difficulty, making it way harder for them to mine the next block, they will try to choose a block that does not increase difficulty.

However, a small room for manipulation remains and if a contract defines a finite time in seconds since the unix epoch a miner may have the option to squeeze in another transaction and fine-tune the timestamp to fit the contracts’ constraints.

The described methods above are standard procedures to check upon smart contract auditing. Once the given constraints are accepted, we can weigh in risks and reasonable solutions. This way of thinking about time in smart contracts has been cut to the quick quite recently …

The gears of missing out

On-Chain gamification is a hot topic: It’s a great way to showcase blockchain potentials, attract newbies to the topic and push the boundaries of what’s possible with the technology nowadays. One of the more notable recent ones is an explicit pyramid scheme named FOMO3D.

The mechanics are quite involved and lovingly put into practice, but the gist of it is that there is a countdown with a maximum of 24 hours and people can buy keys using ether within that time and that ether is added to a prize pool. Each buy increases the countdown by 30 seconds. If the countdown is over, the owner of the last key is awarded with the pool.

It generated quite a hype and people assumed that it won’t end in the near future as there will always be someone who will buy a key in order to get a hit on the prize.

This was until an admittedly creative attacker successfully claimed the pool by introducing a new degree of freedom to the variable time: network congestion.

The attacker set up a smart contract that was called when the timer was about to run out soon. If that was the case and the contract didn’t own the last key it was bought. If it was the case and the contract was the owner, it did call assert(false).

Why’s that? Calling assert(false) uses up the whole amount of gas that one set as limit for a transaction. The attacker set a very high gas limit. Hence, there was a lot to earn for the miner.

The attacker knew that mining pools usually order incoming transactions by their gas price, so the more you are willing to pay in fees, the earlier your transaction is included in a block. Since each block can only contain transactions adding up to a certain amount of gas consumption (~8,000,000), the attacker was able to drastically reduce the probability that attempts to buy keys are included.

The actual attack was a bit more complex than described and is fancy to analyse, which is what the folks over at SECBIT did really well.

However, the punchline for our discussion is quite simple: The time you have available to get a transaction into a block until a specific constraint in a smart contract is no longer met may be in practice drastically reduced if the network is running over capacity — meaning you can’t get your transaction in as long as you are not the one willing to spend the most ether on transaction fees.

In the case of the attack it was something around 4 ETH per block, required for multiple consecutive blocks. However, that’s not much if you can grab the 2.9 M $ jackpot and run.

Auditing consequences

Maybe less groundbreaking than Einstein’s work on relativity, the FOMO3D hack may still require our little but growing group of people who care about smart contract security to think differently about time within our decentralized universe.

The introduced attack vector is inherent to the protocol. We have to accept that we cannot fix it with simple bugfixes. It must be addressed on an architectonical level.

Network congestion is beyond the scope of the runtime environment and the execution context of a smart contract. Reasoning beyond context boundaries requires a special skill at which Albert Einstein excelled: Auditors need to think out-of-the-box.

--

--

Christian Peters
Solidified

I'm a software architect & blockchain engineer from the Berlin startup scene.