Smart Contract Best Practices Revisited: Block Number vs. Timestamp

Phillip Goldberg

Last November, Spankchain, a blockchain for the adult entertainment industry, they informed investors that they had extrapolated the end date two days ahead. The culprit? With the assumptions that block confirmations typically run 15 seconds, they extrapolated the estimated block.number with auction_complete

modifier auction_complete {
require(auctionEndBlock <= block.number || currentAuctionState == AuctionState.success || currentAuctionState == AuctionState.cancel)
_;}

Unfortunately, this assumption turned out to be incorrect, as the average block time turned out to be 13.5–14s, leading to a huge discrepancy in the end date. This incident helps to highlight confusion with block number and timestamps in the EVM. In this article, we are going to explore the block number and timestamp on the EVM, how they are used to ascribe event outcomes, and how we move forward in evaluating different uses.

Timestamp

What is a timestamp exactly and how does it come about? Let’s say there’s a simple contract that stores the timestamp of the block in which it was mined. It would look something like this

pragma solidity 0.4.11
contract getTime {
uint myTime = now;
}

Compiling the contract and looking into the EVM instructions, one finds

05 TIMESTAMP
06 PUSH1 00
08 SSTORE

At the 5th instruction, TIMESTAMP is called and subsequently stored in the variable myTime. Where does the timestamp come from? Defined in the yellow paper as a “scalar value equal to the reasonable output of Unix's time()” , popular ethereum clients, geth and parity, have slightly different definitions block header validity. Both clients require that the node's internal NTP protocol does not drift from pool.ntp.org by more than 10 seconds(geth, parity ). When it comes to validating a block, however, it's a different story. Geth and parity both check if the timestamp is greater than the parent's timestamp, but geth will outright reject any block that has its current time set ahead of the node’s current time. Given the protocol, even if a malicious miner tries to “game” a timestamp, there exists not only the possibility Geth would reject the node, other miners also would be more incentivized to accept an earlier, more accurately timestamped block. This is the most sought-after outcome, otherwise, the miners would lose their rewards from the orphaned block.

Manipulability

Consider this alarm contract, based off of PonziGovermental contract

tick = block.timestamp; // in constructor
function defuseAlarm() returns(bool){
if(block.timestamp > tick+TWELVE_HOURS) {
tick = block.timestamp;
// … alarm remains alive
return false;
} else {
// stop alarm …
return true;
}}

In this contract, the tick is initialized in the constructor. As long the caller of defuseAlarm sends a transaction within 12 hours, the alarm gets defused. However, assume that the only person who calls defuseAlarm does it with 1 second before the alarm sounds. Without an interfering miner, the alarm would be defused. But because timestamp is manipulable, the miner could set a timestamp 2 seconds in the future and prevent the alarm from getting defused (another transaction is needed to stop the alarm)

Block Number

Block numbers, on the other hand, come about from a bit different context. Compiling a contract to get the block number one finds

05 NUMBER
06 PUSH1 00
08 SSTORE

When NUMBER is called, it returns the block number of the parent increased by one. Block numbers persist over all of the nodes in the system, and the restriction is held for all Ethereum clients trying to sync. The block number is a variable where there can exist tamper-proof temporal settlements with Dapps. In this context, it seems as if block number is a much better substitute for time-dependent computation. This is true, as it is a way more reliable source of truth than the timestamp, but it lacks the readability we desire when implementing it. You wouldn’t set your alarm to block 19284832, but rather to a UTC standard date.

Uses of Each

So what considerations should one undertake evaluating block.number and block.timestamp? Block.number is guaranteed by the protocol to increase by one compared to the previous block, while block.timestamp can be minimally gamed by a miner. As mentioned in the discussion of how geth and parity resolve timestamp, a general rule of thumb is your contract can tolerate a 30-second timestamp variation and maintain integrity, then it is safe to use a timestamp.

Let's reconstruct the auction_complete modifier with a timestamp dependence

modifier auction_complete {
require(auctionEndTime <= now || currentAuctionState == AuctionState.success || currentAuctionState == AuctionState.cancel);
_;}

When based off of timestamp, the auction is resolved much more accurately, off by a matter of minutes rather than days. With factors like the difficulty bomb and hard/soft fork upgrades to the network, accurately extrapolating block.number to a time is dangerous and can lead to serious discrepancies in expectations. This is essentially the tradeoff being made when weighing the two options.

If miner manipulation of the timestamp comes at a cost of integrity to your smart contract, then block.number can be a recourse if used in the right context. Let's propose we have a contract Reward, that will decide on a winner of a payout after 200 blocks. It would use a modifier such as this:

modifier lottery_complete {
require(block.number>=startingBlock+200);
_;}

Even with the best-informed on block confirmation times, there exists a vulnerability of uncertainty on when the contract would execute on the modifier. This would be a situation where it’s more important that 200 blocks have passed, rather finding a winner every hour or so.

Conclusion

To recap, block.number and block.timestamp hold different utilities in constructing robust and secure smart contracts. Block numbers cannot be manipulated by miners however it can lead to larger than expected time differences; timestamps are much simpler to base temporal logic off of.

Do you love this stuff?

I’m part of the team at ConsenSys Diligence. If you have a knack for diving deeply into Solidity and the EVM, and an interest in smart contract security, we’re looking for people to join our team (Apply here).

Thanks to Maurelian, Joseph C, and Gerhard Wagner

Phillip Goldberg

Written by

engineer @ dYdX

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade