Solidity Fixed Point Numbers and Address Types (Howto)

With this article, we continue our journey through the realm of Solidity data types.

πŸ‘‰ Recommended Tutorial: Solidity Basic Data Types

This time, we will focus on fixed point numbers, address types and address type members, and investigate when, how, and why they are used.

Traditionally, our articles are running alongside the official Solidity documentation and are used to clarify, supplement, and explain the concepts laid out in the documentation.

Fixed Point Numbers

⚑ Warning: Solidity still does not fully support fixed-point numbers, in that it allows only the declaration, but not the assignment from and to fixed point variables.

Fixed point numbers exist in the signed (fixed) and unsigned (ufixed) variants and sizes.

Keywords can be written in a general form of ufixedMxN and fixedMxN, where the parameter M represents the number of bits that our type takes, and the parameter N represents how many decimal points are reserved for our type.

As with the integer type, the parameter M starts with the value 8 and also increments in the steps of 8, up to the value of 256. The parameter N can be any integer between 0 and 80 (included).

If we express our fixed point type as fixed or ufixed (without the parameters M and N), it is interpreted as an alias for fixed128x18 and ufixed128x18.

Fixed Point Operators

Here we’ll list the available operators for fixed point types:

  • Comparisons: <=, <, ==, !=, >=, > (evaluate to bool)
  • Arithmetic operators: +, -, unary -, *, /, % (modulo)

Fixed vs Floating Point Numbers

We have to make a point about the difference between floating point and fixed point numbers.

Floating point numbers usually correspond to float and double in many programming languages; technically speaking, they’re referred to as IEEE 754 numbers, named after their defining standard IEEE 754.

The main difference is that with the fixed point numbers, the number of bits used for the integer and the fractional part (after the decimal dot) is strictly defined, while with the floating point numbers the number of bits used for the integer and the fractional part is flexible.

In practice, floating point numbers take up almost the entire space to represent the number, and only a small number of bits will be used to define the location of the decimal point.

Address Type

The address type is available in two variants, and they are almost identical but don’t worry, we’ll touch on the difference in a moment:

  • address: The type is associated with a 20-byte value, which is a size of an Ethereum address.
  • address payable: The type is the same as address, the only difference is that it has additional members transfer(...) and send(...).

The difference between the two lies in a need to distinguish an address, i.e., to address payable we can send Ether, from a plain address that holds a smart contract not designed to receive Ether.

In other words, the two address types exist to prevent sending the currency to the wrong address, while we have to explicitly specify the right address.

How to Convert “address” to “address payable” in Solidity?

Of course, in some cases, we’ll have to be able to convert the address type to the address payable type, and for that, we use the explicit conversion function payable(<address>). Conversion in the other direction, i.e. from the address payable type to address type is implicit.

Explicitly conversions to and from the address type are allowed for uint160, integer literals (variants int8/uint8 to int256/uint256), bytes20, and contract type.

Only variables of type address and contract type can be converted to the type address payable by using the explicit conversion function payable(<address>) and payable(this). The keyword this is a reference to the current contract.

In case we’re converting a contract type, the contract must be able to receive Ether, i.e. it either has a receive() function or a payable fallback function.

However, there is an exception to the rule: the expression payable(0).

πŸ’‘ Note: When we’re facing a requirement of needing an address variable and also plan to send Ether to this address, we should declare the address type as address payable as soon as possible to avoid any unnecessary conversions.

Address Type Comparison Operators

The operators available for use with the address type are the comparison operators:

  • <= Less than or equal to
  • < Less than
  • == Equal to
  • != Not equal to
  • >= Greater than or equal to
  • > Greater than

Warning: since there is a possibility to convert a larger type, e.g. bytes32 to an address, the address will be truncated. Solidity creators made a step in reducing this ambiguity since Solidity compiler v0.4.24, forcing us to explicitly truncate the larger type variable.

If we take a 32-byte value

b = 0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC,

we can convert it by keeping the first 20 bytes, like in

address(uint160(bytes20(b))) = 0x111122223333444455556666777788889999AAAA,

or the last 20 bytes, like in

address(uint160(uint256(b))) = 0x777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC.

πŸ’‘ Note: The types address and address payable were introduced with Solidity v0.5.0. One more novelty that this version introduced was that the contracts stopped being derived from the address type, although they can still be explicitly converted to types address or address payable, but only if they have a receive() or payable fallback function.

πŸ‘‰ Recommended Tutorial: What is Solidity payable?

Address Type Members

The two notable members of an address or address payable type are property balance and function transfer(...).

  • The property balance enables us to check the balance of an address.
  • The function transfer(...) allows us to send Ether expressed in units of Wei to a payable address.

Both use cases are shown in the example:

address payable x = payable(0x123);
address myAddress = address(this);
if (x.balance < 10 && myAddress.balance >= 10) x.transfer(10);

The transfer function will fail if the balance on the current contract is smaller than 10 Wei or if the receiving account rejects the transfer. In case of a failure, the transfer() function will revert.

πŸ’‘ Note: If x in the source code above holds a contract address, its source code, i.e. its function for receiving Ether or its fallback function, will be executed with the call of the transfer(...) function. This behavior is an inherent, non-optional feature of EVM. If the execution of these functions depletes the attached gas or fails in some other way, the Ether transfer will be reverted, and the current contract will terminate, raising an exception.

ℹ️ Info: “The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function” (docs).

Speaking of currency (Ether) transfer, the send(...) function represents the low-level variant of the transfer(...) function. If the send(...) function execution fails, the current contract will not terminate throwing an exception, but instead, send(...) will return a boolean value false.

Warning: The send(...) function also brings some dangers with it, e.g. the transfer will fail if the call stack depth reached 1024, and the caller can force such an event.

Also, the transfer will fail if the currency recipient runs out of gas. Therefore, good programming patterns for safe Ether transfer include checking the return value of the send(...) function or using the transfer(...) function, but the best pattern is the one where the recipient withdraws the currency.

When we want to interface with the contracts that are not compliant with the ABI or get direct control over the encoding process, we would use the functions call(...), delegatecall(...), and staticcall(...).

Their input parameter is a single bytes memory and the return results are the success condition (bool type) and the returned data (bytes memory type, same as the input parameter).

The functions abi.encode(...), abi.encodePacked(...), abi.encodeWithSelector(...), and abi.encodeWithSignature(...) are available for encoding the structured data:

bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
(bool success, bytes memory returnData) = address(nameReg).call(payload);
require(success);

Warning: since abi. functions mentioned above are low-level functions, they should be used with caution, i.e. an unknown contract may be malicious, and if we call it, we’re giving it control that might be used to call our contract in return.

In that case, we should prepare for contract state changes with the call return. The regular way of interacting with other contracts is calling a function exposed on a contract x‘s object, e.g. x.f().

The amounts of supplied gas and Ether can be adjusted during the function call by supplying the gas and Ether modifiers:

address(nameReg).call{
   gas: 1000000, value: 1 ether
}(abi.encodeWithSignature("register(string)", "MyName"));

Of course, we could’ve placed the entire call(...) function call in the same line, but I split it into three lines for better readability.

Similar to the call(...) function, the delegatecall(...) function can also be called, but as mentioned a few articles before, only the code of the given address (contract) will be used, and everything else, such as storage, balance, etc. will be left out.

Let’s remind ourselves, delegatecall(...) can only call the library code stored in this other contract. However, it is up to us as users to check if the layout of storage in both contracts is suitable for calling the delegatecall(...).

πŸ’‘ Note: Before the EVM version Homestead, only a limited variant of the delegatecall(...) function named callcode(...) was available, but it didn’t provide access to the original msg.sender and msg.value values. callcode(...) was removed in Solidity v0.5.0.

πŸ’‘ Further Note: The EVM version Byzantium introduced the staticcall(...) function. It can be used the same as the call(...) function, however it will revert if the called function modifies the contract state.

We should have in mind that all three functions, call(...), delegatecall(...), and staticcall(...) work on a very low level and should be used only when there’s no other way to achieve the required functionality because the use of these functions violates the type-safety of Solidity.

The gas option is available on all three methods, while the value option is only available on the call(…) function.

πŸ’‘ Note: The best practice is to avoid using hardcoded gas values in our smart contracts, regardless of the access to the state variables.

There are two more noteworthy address type members in the context of our story: code and codehash.

  • Member <address>.code enables us to query the deployed code for a smart contract, i.e. to get the EVM bytecode in the form of bytes memory, which can be empty.
  • Member <address>.codehash provides us with the Keccak-256 hash of the deployed source code in the form of bytes32.

We should be aware that <address>.codehash is cheaper than calling the function keccak256(addr.code).

As was mentioned a few articles ago, every contract can be converted to the address type, so getting the contract’s balance is possible via address(this).balance.

Conclusion

With this article, we made three more very important steps in the direction of getting to know Solidity data types, namely fixed-point numbers, address, and address type members.

What we get by closely knowing Solidity data types being able to select the right data type in a specific situation.

Also, this knowledge gives us the fighting chance to avoid common mistakes and pitfalls during the design and coding phases in creating our smart contracts.

First, we made a polite and gentle introduction to today’s topics.

Second, we explained what fixed point numbers are, made a short comparison with the floating point numbers, and jumped on the next, important point.

Third, we never before got so close with addresses. Any closer, and we’d go postal.

Fourth, we jumped into a detailed discussion about address type members (functions and properties). It got quite technical, but there’s no better way of learning than poppin’ the hood.


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):