Solidity Data Types

Solidity Data Types

Solidity Data Types

Solidity is a statically typed language, which means that the type of each variable (state and local) needs to be specified. Solidity provides several elementary types which can be combined to form complex types.

There are two types of data types in solidity.

  1. Value Type
  2. Reference Type

Value Types

The following types are also called value types because variables of these types will always be passed by value, i.e. they are always copied when they are used as function arguments or in assignments.


Booleans of the Solidity value types can be either true or false. The boolean is defined with the bool keyword.

It works with these operators:

  • ! (logical negation)
  • && (logical conjunction, AND)
  • || (logical disjunction, OR)
  • == (equality)
  • != (inequality)

There are two main Solidity types of integers of differing sizes:

  • int – signed integers.
  • uint – unsigned integers.

Speaking of size, to specify it, you have keywords such as uint8 up to uint256, that is, of 8 to 256 bits. The simple uint and int are similar to uint256 and int256, respectively.

Integers work with the following operators:

Comparison operators (evaluates to bool)

  • <= (less than or equal)
  • < (less than)
  • == (equal to)
  • != (not equal to)
  • >= (greater than or equal)
  • > (greater than)

Bit operators

  • & (bitwise AND)
  • | (bitwise inclusive OR)
  • ^ (bitwise XOR (exclusive OR))
  • ~ (bitwise NOT)

Arithmetic operators

  • + (addition)
  • - (subtraction)
  • unary - (subtract on a single operand)
  • unary + (add on a single operand)
  • * (multiply a single operand)
  • / (division)
  • % (remainder (of division))
  • ** (exponentiation)
  • << (left shift)
  • >> (right shift)

Fixed Point Numbers

There are two types of fixed point numbers:

  • fixed – signed fixed point number.
  • ufixed – unsigned fixed point number.

This value type also can be declared keywords such as ufixedMxN and fixedMxN. The M represents the amount of bits that the type takes, with N representing the number of decimal points that are available. M has to be divisible by 8, and a number from 8 to 256. N has to be a value between 0 and 80, also being inclusive.

Note: fixed point numbers can be declared in Solidity, but they are not completely supported by this language.

The fixed point numbers function with these operators:

Comparison operators (evaluates to bool)

  • <= (less than or equal)
  • < (less than)
  • == (equal to)
  • != (not equal to)
  • >= (greater than or equal)
  • > (greater than)

Arithmetic operators

  • + (addition)
  • - (subtraction)
  • unary - (subtract on a single operand)
  • unary+ (add on a single operand)
  • * (multiply a single operand)
  • / (division)
  • % (remainder (of division))


The address Solidity value type has two similar kinds:

  • address holds a 20-byte value (size of an Ethereum address).
  • address payable is the same as address, but have transfer and send members.

Note: there are two distinctions between smart contract addresses – the address payable can receive Ether, while simple address cannot.

Implicit conversions of addresses:

  • From address payable to address: allowed.
  • From address to address payable: not allowed. This type of conversion is only possible with intermediate conversion to uint160.
  • Address literals can be converted to address payable.

Explicit conversions to and from address are permitted for integers, integer literals, contact types and bytes20. However, Solidity prevents the conversions of address payable(x).

The address(x) can be converted to address payable in cases when x is of integer, fixed bytes type, or a literal or a contract that has a payable fallback function. When x is a contract without the payable fallback function, the address(x) is of type address.

Note: it is possible to disregard the difference between address and address payable by using the address. You can call the transfer function on msg.sender which is an address payable.

Members of Address

The balance property queries the balance of an address, while Ether can be sent to addresses with the transfer function.

The transfer function queries the balance of an address by applying the property balance and sending Ether (in units of wei) to a payable address:

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

Gain more direct control over encoding or interface with contracts (not adhere to the ABI) with calldelegatecall and staticcall functions.

They accept one bytes memory parameter, deliver the success condition as a boolean and the returned data. For encoding of structured data, use abi.encodeabi.encodePackedabi.encodeWithSelector and abi.encodeWithSignature:

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

You can adjust the provided gas using the .gas() modifier:

					address(nameReg).call.gas(1000000)(abi.encodeWithSignature("register(string)", "MyName"));

You can manipulate the Ether value as well:

					address(nameReg).call.value(1 ether)(abi.encodeWithSignature("register(string)", "MyName"));

It is possible to combine these modifiers. Their order is not important:

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

Fixed-size Byte Arrays

The value types bytes1bytes2bytes3, …, bytes32 contain a sequence of bytes (from 1 to 32).

Operators that can be applied to this Solidity value type:

  • Comparisons: <=<==!=>=> (evaluate to bool)
  • Bit operators: &|^ (bitwise exclusive or), ~ (bitwise negation)
  • Shift operators: << (left shift), >> (right shift)
  • Index access: If x is of type bytesI, then x[k] for 0 <= k < I returns the k th byte (read-only).

Note: there is one possible parameter of .length. It provides the fixed length of the byte array (read-only).


Enums generate user-defined Solidity types. The explicit conversion is possible to and from all integer types, but the implicit conversion is not.

Note: during the explicit conversion from integer, it is confirmed whether the value at runtime is inside the range of the enum. If this is not confirmed, a failing assert occurs.

					pragma solidity >=0.4.16 <0.7.0;

contract test {
    enum ActionChoices { GoLeft, GoRight, GoStraight, SitStill }
    ActionChoices choice;
    ActionChoices constant defaultChoice = ActionChoices.GoStraight;

    function setGoStraight() public {
        choice = ActionChoices.GoStraight;

    function getChoice() public view returns (ActionChoices) {
        return choice;

    function getDefaultChoice() public pure returns (uint) {
        return uint(defaultChoice);

Reference Types

Solidity reference types have to be handled with more caution than value types. It is crucial to clearly indicate the data area where the reference type is stored: memorystorage or calldata. Reference type values are manipulated via more than one different name.

Data Location and Assignment Behavior

Every reference type contains information on where it is stored. There are three possible options: memorystorage, and calldata. The set location is important for semantics of assignments, not only for the persistence of data:

  • Assignments between storage and memory (or from calldata) always generate an independent copy.
  • Assignments from memory to memory only create references. This means that changes to one memory variable are also visible in all other memory variables that refer to the same data.
  • Assignments from storage to a local storage variable also only assign a reference.
  • All other assignments to storage always copy. Examples for this case are assignments to state variables or to members of local variables of storage struct type, even if the local variable itself is just a reference.

In the following example, the location of x is storage, while the location of memoryArray is in memory:

					pragma solidity >=0.4.0 <0.7.0;

contract C {
    uint[] x;

    function f(uint[] memory memoryArray) public {
        x = memoryArray; // works, copies the whole array to storage
        uint[] storage y = x; // works, assigns a pointer, data location of y is storage
        y[7]; // fine, returns the 8th element
        y.length = 2; // fine, modifies x through y
        delete x; // fine, clears the array, also modifies y
        // The following does not work; it would need to create a new temporary /
        // unnamed array in storage, but storage is "statically" allocated:
        // y = memoryArray;
        // This does not work either, since it would "reset" the pointer, but there
        // is no sensible location it could point to.
        // delete y;
        g(x); // calls g, handing over a reference to x
        h(x); // calls h and creates an independent, temporary copy in memory

    function g(uint[] storage) internal pure {}
    function h(uint[] memory) public pure {}


Arrays can have fixed or dynamic sizes for the compiling process. The array with fixed size k and element type T is combined as T[k]. The dynamically-sized array is T[].

An array consisting of 6 dynamic arrays of uint looks like this: uint[] [6]. By default in Solidity, x[3] array consists of three elements of type despite the fact that it can be an array.

Note: array elements can have any type. However, there are some limitations: mappings must be stored in the storage data location and public functions are to have ABI type parameters.

Allocating Memory Arrays

The keyword new creates arrays with a runtime-dependent length in memory.

Remember: differently than storage arrays, size of memory arrays cannot be manipulated. Determine the size beforehand or generate a new array and transfer all elements to it.

					pragma solidity >=0.4.16 <0.7.0;

contract C {
    function f(uint len) public pure {
        uint[] memory a = new uint[](7);
        bytes memory b = new bytes(len);
        assert(a.length == 7);
        assert(b.length == len);
        a[6] = 8;

Array Literals

A list of one or multiple expressions, separated by commas and placed in square brackets ([…]) is an array literal. It is a statically-sized memory array.

The following example shows that the type of [1, 2, 3] is uint8[3] memory. Since every constant is of uint8 type, the first element must be converted to uint before it can be uint[3] memory.

					pragma solidity >=0.4.16 <0.7.0;

contract C {
    function f() public pure {
        g([uint(1), 2, 3]);
    function g(uint[3] memory) public pure {
        // ...

The next example shows an impossible situation – fixed size memory arrays cannot be set to dynamically-sized memory arrays. Since it cannot compile, a type error appears (unit[3] cannot be converted to uint[] memory):

					pragma solidity >=0.4.0 <0.7.0;

contract C {
    function f() public {
        uint[] memory x = [uint(1), 3, 4];

Array Members

Arrays have these members:

  • length – indicates the number of elements. For memory arrays, the length is fixed after they are generated. However, it can be dynamic and can rely on runtime parameters. The length is set to dynamically-sized arrays to change their size.
  • push – attaches an element at the end of the dynamic storage arrays and bytes (not string). The newly-added element is zero-initialized.
  • pop removes an element at the end of the dynamic storage arrays and bytes (not string).
					pragma solidity >=0.4.16 <0.7.0;

contract ArrayContract {
    uint[2**20] m_aLotOfIntegers;
    // Note that the following is not a pair of dynamic arrays but a
    // dynamic array of pairs (i.e. of fixed size arrays of length two).
    // Because of that, T[] is always a dynamic array of T, even if T
    // itself is an array.
    // Data location for all state variables is storage.
    bool[2][] m_pairsOfFlags;

    // newPairs is stored in memory - the only possibility
    // for public contract function arguments
    function setAllFlagPairs(bool[2][] memory newPairs) public {
        // assignment to a storage array performs a copy of ``newPairs`` and
        // replaces the complete array ``m_pairsOfFlags``.
        m_pairsOfFlags = newPairs;

    struct StructType {
        uint[] contents;
        uint moreInfo;
    StructType s;

    function f(uint[] memory c) public {
        // stores a reference to ``s`` in ``g``
        StructType storage g = s;
        // also changes ``s.moreInfo``.
        g.moreInfo = 2;
        // assigns a copy because ``g.contents``
        // is not a local variable, but a member of
        // a local variable.
        g.contents = c;

    function setFlagPair(uint index, bool flagA, bool flagB) public {
        // access to a non-existing index will throw an exception
        m_pairsOfFlags[index][0] = flagA;
        m_pairsOfFlags[index][1] = flagB;

    function changeFlagArraySize(uint newSize) public {
        // if the new size is smaller, removed array elements will be cleared
        m_pairsOfFlags.length = newSize;

    function clear() public {
        // these clear the arrays completely
        delete m_pairsOfFlags;
        delete m_aLotOfIntegers;
        // identical effect here
        m_pairsOfFlags.length = 0;

    bytes m_byteData;

    function byteArrays(bytes memory data) public {
        // byte arrays ("bytes") are different as they are stored without padding,
        // but can be treated identical to "uint8[]"
        m_byteData = data;
        m_byteData.length += 7;
        m_byteData[3] = 0x08;
        delete m_byteData[2];

    function addFlag(bool[2] memory flag) public returns (uint) {
        return m_pairsOfFlags.push(flag);

    function createMemoryArray(uint size) public pure returns (bytes memory) {
        // Dynamic memory arrays are created using `new`:
        uint[2][] memory arrayOfPairs = new uint[2][](size);

        // Inline arrays are always statically-sized and if you only
        // use literals, you have to provide at least one type.
        arrayOfPairs[0] = [uint(1), 2];

        // Create a dynamic byte array:
        bytes memory b = new bytes(200);
        for (uint i = 0; i < b.length; i++)
            b[i] = byte(uint8(i));
        return b;


Solidity allows to define new types in the form of structs. This process is shown in the example below:

					pragma solidity >=0.4.11 <0.7.0;

contract CrowdFunding {
    // Defines a new type with two fields.
    struct Funder {
        address addr;
        uint amount;

    struct Campaign {
        address payable beneficiary;
        uint fundingGoal;
        uint numFunders;
        uint amount;
        mapping (uint => Funder) funders;

    uint numCampaigns;
    mapping (uint => Campaign) campaigns;

    function newCampaign(address payable beneficiary, uint goal) public returns (uint campaignID) {
        campaignID = numCampaigns++; // campaignID is return variable
        // Creates new struct in memory and copies it to storage.
        // We leave out the mapping type, because it is not valid in memory.
        // If structs are copied (even from storage to storage),
        // types that are not valid outside of storage (ex. mappings and array of mappings)
        // are always omitted, because they cannot be enumerated.
        campaigns[campaignID] = Campaign(beneficiary, goal, 0, 0);

    function contribute(uint campaignID) public payable {
        Campaign storage c = campaigns[campaignID];
        // Creates a new temporary memory struct, initialised with the given values
        // and copies it over to storage.
        // Note that you can also use Funder(msg.sender, msg.value) to initialise.
        c.funders[c.numFunders++] = Funder({addr: msg.sender, amount: msg.value});
        c.amount += msg.value;

    function checkGoalReached(uint campaignID) public returns (bool reached) {
        Campaign storage c = campaigns[campaignID];
        if (c.amount < c.fundingGoal)
            return false;
        uint amount = c.amount;
        c.amount = 0;
        return true;

The contract is not as efficient as a crowdfunding contract, but it has the necessary parts for you to learn structs. They can be applied or contain mappings and arrays.

Note: a struct cannot have a member of the same type as its own.

In all of the functions in the example, a struct type is assigned to a local variable with data location storage. Therefore, it does not copy a struct. Instead, a reference is stored so that assignments to members of the local variable write to the state.

Like in campaigns[campaignID].amount = 0, it is possible to directly access the struct members and not assign it to a local variable.

Mapping Types 

Syntax of (_KeyType =>_ValueType) defines the mapping types.

  • The _KeyType can be any elementary type (plus bytes and string). However, contract types, enums, mappings, structs, and any array type apart are not permitted.
  • _ValueType accepts any type, mappings as well.

Note: Solidity mappings can only have a data location of storage. Therefore, they are allowed for state variables.

  • It is possible to set state variables of mapping Solidity type as public and the language generates a getter. Then, the _KeyType is a parameter for the getter.
  • In cases when _ValueType is a value type or a struct, the getter returns _ValueType. When it is an array or mapping, the getter has one parameter for every _KeyType, recursively.

Take a look at the example of mapping Solidity:

					pragma solidity >=0.4.0 <0.7.0;

contract MappingExample {
    mapping(address => uint) public balances;

    function update(uint newBalance) public {
        balances[msg.sender] = newBalance;

contract MappingUser {
    function f() public returns (uint) {
        MappingExample m = new MappingExample();
        return m.balances(address(this));

Operators Involving LValues

The a is an LValue (for instance, a variable or something that can be assigned to). It has operators as shorthands:

  • a += e is the same as a= a + e. The operators -=*=/=%=|=&= and ^= are defined accordingly.
  • a++ and a-- is the same as a += 1 / a -= 1 but the expression still has the previous a value.
  • --a and ++a work the same on a but return the value after the change.


In the following example, delete x assigns x to 0 and does not affect data. delete data sets data to 0, does not affect x:

					pragma solidity >=0.4.0 <0.7.0;

contract DeleteExample {
    uint data;
    uint[] dataArray;

    function f() public {
        uint x = data;
        delete x; 
        delete data;
        uint[] storage y = dataArray;
        delete dataArray; // this sets dataArray.length to zero, but as uint[] is a complex object, also
        // y is affected which is an alias to the storage object
        // On the other hand: "delete y" is not valid, as assignments to local variables
        // referencing storage objects can only be made from existing storage objects.
        assert(y.length == 0);
  • The delete a sets the initial value for the type to a. I.e. for integers it is equivalent to a = 0.
  • It is possible to apply it on arrays. It will set a dynamic array of length zero or static array of the same length with all elements assigned their initial value.
  • delete a[x] will remove an element at the specified array index (won’t change other items and length). This results in a gap in an array.
  • It sets a struct with all members reset. The a value after delete a is the same as delete does not influence mappings. By deleting a struct, you reset all members that are not related to mappings.

Conversions Between Elementary Types

Implicit Conversions

When you use operators on different Solidity types, the compiler aims to implicitly convert one of the operands to the type of the other.

Therefore, the operations execute in the type of one of the operands. The implicit conversion takes place without any issues if it is semantically logic, and data is not lost.

For instance, you can convert uint8 to uint16 and int128 to int256. However, int8 cannot be converted to uint256 (it can’t hold values like -1).

Explicit Conversions 

When you are confident that a conversion will take place correctly, attempt to perform the explicit type conversion.

Warning: be careful since such actions can render unpredictable results and disregard some security features.

The example below depicts the conversion of a negative int to a uint:

					int  y = -3;
uint x = uint(y);

At the end of this conversion, the x will have the value of 0xfffff..fd (64 hex characters). It is -3 in the two’s complement representation of 256 bits.

In cases when integers are converted into a smaller type, higher-order bits are removed. After the second line is compiled, b will be 0x5678:

					uint32 a = 0x12345678;
uint16 b = uint16(a);

The next example shows the opposite situation when an integer is converted to larger Solidity types. It will be padded at the higher order end. After the second line, b will be 0x00001234. Comparison between equal and the original integer is the result:

					uint16 a = 0x1234;
uint32 b = uint32(a);
assert(a == b);

The fixed-size bytes Solidity types act differently during conversions. After the second line, b will be 0x12. If you try to convert them to smaller type, the sequence will be disturbed:

					bytes2 a = 0x1234;
bytes1 b = bytes1(a);

In cases when fixed-size bytes type is converted to a larger type, it is padded on the right. After the second line, b will be 0x12340000:

					bytes2 a = 0x1234;
bytes4 b = bytes4(a);
assert(a[0] == b[0]);
assert(a[1] == b[1]);

During truncating or padding, integers and fixed-size byte types act differently. Therefore, explicit conversions between them are not permitted (unless their sizes are identical). To convert them from different sizes, intermediate conversions make the truncation and padding rules explicit:

					bytes2 a = 0x1234;
uint32 b = uint16(a); // b will be 0x00001234
uint32 c = uint32(bytes4(a)); // c will be 0x12340000
uint8 d = uint8(uint16(a)); // d will be 0x34
uint8 e = uint8(bytes1(a)); // e will be 0x12

Leave a Reply


We connect you to a world of houseplants and urban gardening tailored to your home


Login to your account