Solidity Ether Units, Time Units, and Global Variables

With this article, we’re opening a new area of our study, that of units and globally available variables in Solidity.

To begin with, we’ll learn about ether units and time units. After that, we’ll start with a block of subsections on special variables and functions, stretching through this and the next two articles.

It’s part of our long-standing tradition to make this (and other) articles a faithful companion, or a supplement to the official Solidity documentation.

Ether Units

When mentioning Ether units of currency, we can express them with a literal number and a suffix of wei, gwei or ether.

These suffixes specify a sub-denomination of Ether. We assume amounts written without a suffix as Wei.

The purpose and effect of using sub-denomination is a multiplication of the denomination by a power (exponent) of ten.

assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 ether == 1e18);

πŸ’‘ Note: There can be found denominations, such as finney and szabo, but they were deprecated in Solidity v0.7.0.

Time Units

Solidity has a nice and natural way of expressing time units with suffixes, such as seconds, minutes, hours, days, and weeks.

Seconds are the base unit, and units are considered to correspond to:

  • 1 == 1 seconds
  • 1 minutes == 60 seconds
  • 1 hours == 60 minutes
  • 1 days == 24 hours
  • 1 weeks == 7 days

When working with calendar calculations using these units, we should take extra care, because only some years have 365 days, and because of leap seconds, not even every day has 24 hours.

Since leap seconds are unpredictable, an exact calendar always has to be updated by an external source (oracle), which motivated Solidity authors to remove the suffix years in Solidity v0.5.0.

We should remember that these suffixes cannot be applied to variables, meaning if we want to interpret a function parameter expressed in days, we can easily do so like in the example below:

function f(uint start, uint daysAfter) public {
    if (block.timestamp >= start + daysAfter * 1 days) {
      // ...
    }
}

Special Variables and Functions

The global namespace contains special variables and functions primarily used to provide us with information about the blockchain. They are also available utility functions for general use.

Block and Transaction Properties

The following is a list of block and transaction properties, as shown  in the official Solidity documentation. Parentheses next to each block/transaction property define the member type:

  • blockhash(uint blockNumber) returns (bytes32): hash of the given block when blocknumber is one of the 256 most recent blocks; otherwise returns zero
  • block.basefee (uint): current block’s base fee (as defined in Ethereum Improvement Proposals EIP-3198 and EIP-1559)
  • block.chainid (uint): current chain id
  • block.coinbase (address payable): current block miner’s address
  • block.difficulty (uint): current block difficulty
  • block.gaslimit (uint): current block gas limit
  • block.number (uint): current block number
  • block.timestamp (uint): current block timestamp as seconds since Unix epoch
  • gasleft() returns (uint256): remaining gas
  • msg.data (bytes calldata): complete calldata
  • msg.sender (address): the sender of the message (current call)
  • msg.sig (bytes4): first four bytes of the calldata (i.e. function identifier)
  • msg.value (uint): number of Wei sent with the message
  • tx.gasprice (uint): the gas price of the transaction
  • tx.origin (address): the sender of the transaction (full call chain)

Note: We should expect the values of all members of msg, including msg.sender and msg.value will change with every external function call. This expectation also applies to library functions.

πŸ’‘ Info: Off-chain computation is simply a computation that takes place outside a blockchain. Oracle networks can provide a trust-minimized form of off-chain computation to extend the capabilities of blockchains – this is known as oracle computation. (Chainlink)

Contracts can be evaluated both off-chain and on-chain, i.e. in the context of a transaction included in a block.

If a contract is evaluated off-chain, we should assume that block.* and tx.* members don’t refer to a specific block or transaction. Instead, the values we’d find in these members are provided by the EVM implementation executing the contract, and therefore, they can be completely arbitrary.

πŸ—’οΈ Note: It is suggested to avoid block.timestamp or blockhash member values as sources of randomness. The reason for this suggestion lies in the fact that, in some instances, miners can manipulate both the timestamp and the block hash. The timestamp of the current block must be strictly larger than the timestamp of the last block, and the only thing we can know for sure is that it will be between the timestamps of two neighboring blocks in the canonical chain; how far it will be from any particular block timestamp is not known.

πŸ’‘ Info: “The word canonical is used to indicate a particular choice from a number of possible conventions. This convention allows a mathematical object or class of objects to be uniquely identified or standardized.” (Wolfram.com)

Only the recent 256 blocks’ hashes are available, due to scalability reasons. A hash of any older block will be zero.

In Solidity versions prior to 0.5, the current blockhash(...) function was previously known and available as block.blockhash(...); the current gasleft(...) function was previously known and available as msg.gas(...).

In Solidity v0.7.0 the now alias (for block.timestamp) was removed.

ABI Encoding and Decoding Functions

The following list contains ABI-appropriate functions for low-level interactions with EVM, as laid out in the official Solidity documentation:

  • abi.decode(bytes memory encodedData, (...)) returns (...): ABI-decodes the given data, while the types are given in parentheses as second argument. Example: (uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))
  • abi.encode(...) returns (bytes memory): ABI-encodes the given arguments
  • abi.encodePacked(...) returns (bytes memory): Performs packed encoding of the given arguments. Note that packed encoding can be ambiguous!
  • abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory): ABI-encodes the given arguments starting from the second and prepends the given four-byte selector
  • abi.encodeWithSignature(string memory signature, ...) returns (bytes memory): Equivalent to abi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)
  • abi.encodeCall(function functionPointer, (...)) returns (bytes memory): ABI-encodes a call to functionPointer with the arguments found in the tuple. Performs a full type-check, ensuring the types match the function signature. Equivalent to abi.encodeWithSelector(functionPointer.selector, (...))

πŸ—’οΈ Note: “These encoding functions can be used to craft data for external function calls without actually calling an external function. Furthermore, keccak256(abi.encodePacked(a, b)) is a way to compute the hash of structured data (although be aware that it is possible to craft a β€œhash collision” using different function parameter types).” (docs)

Conclusion

In this article, we learned about ether and time units, followed by special variables and functions.

First, we introduced ether units and discussed the use of the main unit and its sub-denominations.

Second, we introduced time units and discussed the possibilities of expressing time in different time units.

Third, we took a closer look at the block and transaction properties, but also listed many of them with descriptions and notes on specific behaviors for a more thorough understanding.

Fourth, we also touched on the topic of ABI encoding and decoding functions, described them, and gave a usage hint in a form of a note.

What’s Next?

This tutorial is part of our extended Solidity documentation with videos and more accessible examples and explanations. You can navigate the series here (all links open in a new tab):