Reading the Block Timestamp in Microseconds
Supra blocks carry a timestamp with microsecond resolution. block.timestamp reports it in whole
seconds, as it does on every EVM chain, and a precompile reports the whole value.
The precompile lives at 0x0000000000000000000000000000000053555004, and the address is frozen.
Quick start
import {LibBlockTimestamp} from "supra_contracts/libraries/LibBlockTimestamp.sol";
contract Clock {
function elapsedSince(uint64 startMicros) external view returns (uint64) {
return LibBlockTimestamp.microseconds() - startMicros;
}
}The supra_contracts import is set up as described in
Importing the Solidity libraries. The
library is convenience only. Calling
ISupraBlockTimestamp(0x0000000000000000000000000000000053555004).microseconds() directly gives the
same behaviour.
What you get
microseconds() returns a uint64: the microseconds since the Unix epoch of the block being
executed. It is the same number SupraMVM’s timestamp::now_microseconds() returns for that
block, so a contract and a Move module reading in the same block read one clock.
block.timestamp is that value divided by 1,000,000. The two always agree:
assert(LibBlockTimestamp.microseconds() / 1_000_000 == block.timestamp);There are no rules about who may read. Every call is served - STATICCALL, a nested CALL, a
DELEGATECALL from a proxy, a constructor, an automated task - because the value is public header
data, identical for every transaction in the block and for every frame of every transaction. There
is nothing about it a caller could influence or gain by reading from one frame rather than another.
A call that carries value is also served; the value sent stays at the precompile’s address, which no
key controls, and cannot be recovered.
The one place a read is refused is genesis contract deployment, which runs before any block
exists and so has no certified timestamp to serve. A constructor that records its own deployment
microsecond works for every ordinary deployment and reverts TimestampUnavailable() if the same
contract is deployed as part of genesis. Handle it, or keep the clock out of the constructor, if
your contract is going into a genesis set.
The read itself costs a fixed 20 gas, the same as BLOCKHASH, on top of the cost of the call.
What it is not
Not a finer-grained ordering signal. Every transaction in a block reads the same value. To tell two transactions of one block apart, use their position in the block, not the clock.
Not a replacement for block.timestamp. Deadlines, timelocks and vesting schedules are written
in seconds across the whole ecosystem, and block.timestamp stays in seconds so that code ported
to Supra behaves as it did. Mixing the two units in one comparison is the mistake to avoid: store
which unit a stored deadline is in, or keep every deadline in one of them.
Reverts
Every refusal except running out of gas reverts with a custom error rather than returning a sentinel, so a zero can never be mistaken for a timestamp. A frame with fewer than 20 gas fails out of gas with empty revert data.
| Error | When |
|---|---|
UnknownSelector() | the call data names something other than microseconds() |
MalformedInput() | fewer than four bytes, or microseconds() followed by extra bytes |
TimestampUnavailable() | there is no certified block behind the execution |
TimestampUnavailable() is reachable from a transaction in one case, genesis contract deployment,
as described above. Otherwise every block execution and every eth_call or eth_estimateGas
supplies the block’s clock, and it is the fail-closed answer for internal node paths that have no
block behind them.
Testing against it
In a Foundry test, mock the address with the vm.mockCall cheat code, the way you would any
other precompile. Hardhat has no vm, so the snippet is Foundry-only:
vm.mockCall(
address(0x0000000000000000000000000000000053555004),
abi.encodeWithSignature("microseconds()"),
abi.encode(uint64(1_700_000_000_000_000))
);/rpc/v1/eth refuses an eth_call or eth_estimateGas that names a past height. On the
wallet-integration endpoint, an eth_estimateGas at a past height runs in that block’s environment,
so the precompile reports that block’s clock; an eth_call there is answered for the head whatever
height is named. Neither answers what a timestamp-reading contract would have done at an earlier
height. See
The wallet-integration endpoint.