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 asaddress
, the only difference is that it has additional memberstransfer(...)
andsend(...)
.
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 apayable
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):