Skip to content

Solidity: Basics

Sumi edited this page Nov 27, 2024 · 11 revisions

Solidity: Smart Contract Language

In the previous section, we learned what a smart contract is and even wrote and executed one. We need to understand more about the smart contract programming language Solidity. Let us start with what it is and why we use it.

Solidity is an object-oriented, statically typed high-level programming language for writing smart contracts for the Ethereum blockchain network. The solidity language is influenced by C++, JavaScript and Python. The syntax of the solidity programming language closely resembles the JavaScript. Solidity is also known as a Contract-Oriented programming language.

Gavin James Wood invented solidity in August 2014. Later it was developed as a programming language targeting the EVM by the Ethereum Solidity team led by Christian Reitwiessner.

Features

  • Supports inheritance including multiple inheritances with C3 linearization.
  • Supports libraries
  • Supports complex user-defined data types like mapping and struct.

Why Solidity, why not an existing language?

Unlike general-purpose applications, smart contracts are a piece of code that runs on EVM. These codes are metered in a very specific way - the instructions have certain monetary costs associated with them, and creating a custom VM gives the freedom to define which instructions will exist and how much they will cost. Whereas retroactively adding cost calculations to existing language, which was never made with that in mind, would probably not work well.

You can also write Ethereum smart contracts in Vyper and LLL (besides just writing EVM bytecode directly). Solidity is JavaScript-like, Vyper is Python-like while LLL is Lisp-like.

The course chooses Solidity due to its huge community support.

Components of a Contract

A contract written in Solidity is somewhat similar to classes in object-oriented languages like Java. Each contract can contain a set of components.

Let's recall the Solidity smart contract example from the previous unit.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Storage {

    uint256 number;

    function store(uint256 num) public {
        number = num;
    }

    function retrieve() public view returns (uint256) {
        return number;
    }
}

Now the components present in the above example are

  • Declaration of State Variables: State variables are variables whose values are permanently stored in contract storage. In the example, the number is the state variable.
  • Declaration & Definition of Functions: Functions are the executable units of code within a contract. In our contract, store() and retrieve() are functions.

Apart from the above components, a solidity contract will have the following components inside its definition.

  • Declaration & Definition of Function Modifiers: Function modifiers can be used to amend the semantics of functions in a declarative way.
  • Declaration of Struct Types: Structs are user-defined data types that can group several variables together.
  • Declaration of Enum Types: Enums can be used to create user-defined data types with a finite set of constant values.
  • Declaration of Events: Events are used to interact with the EVM logging functionalities

Basic Data Types

Integer

Integer data types are used to store numbers in Solidity contracts. There are two types of integer data types available in Solidity language.

  • uint - is used to store unsigned integer values, representing only the positive integers along with zero. Keywords uint8 to uint256 in steps of 8 are used to define unsigned integer from 8 bits to 256 bits. uint256 is the same as uint.
  • int - is used to store signed integer values, representing both positive and negative integer numbers along with zero. Keywords int8 to int256 in steps of 8 are used to define signed integer from 8 bits to 256 bits. int256 is the same as int.

Integers in Solidity are restricted into certain range: uint32 means that it can contain values from 0 to 2^32 - 1 (^ exponential, and -1 because 0 takes up one space). If any operations of two uint32 numbers exceed these ranges, the result will be truncated which may give inaccurate results. The same is true for the int data type.

To declare a uint variable, we use the appropriate keyword for the required size, followed by the name of the variable. It is also possible to assign a value to the variable at the time of defining it.

To create a uint variable the following syntax is used:

uint variable_name = value

String

Solidity program supports string literals, these are written with either double or single quotes ("foo" or 'bar'). To declare a string variable, we use the keyword string followed by the name of the variable and the optional assignment of value to the variable.

To create a string variable the following syntax is used:

string variable_name = value

Address

Remember, we learned about EOA and Contract Accounts in the Accounts unit. The address type is a special type provided by Solidity to store EOA and Contract Account addresses. The size of an address is 20 bytes.

To create an address variable the following syntax is used:

address variable_name = value

The address comes in two identical flavors:

  • address: which is used to hold the Ethereum address of the size 20 bytes.
  • address payable: it is the same as the address but with additional member functions transfer and send. Basically, we can send ether to address payable but not to address.

address payable can be implicitly converted to address but to convert from address to address payable you have to do it explicitly by using payable(address).

Supported Operators: <=, <, ==, !=, >= and >

Members of address payable:

  • balance: to get the balance of a corresponding address
  • transfer: to transfer ether to an address from the contract address (the contract will be the one from which it was invoked). In case the transfer is rejected or failed, the function will automatically revert the transaction. The transfer has a low-level counterpart known as send, which does the same but will not revert the transaction, instead will return a false value upon failure.

Enum

Enums restrict a variable to have one of a few predefined values. Enums can be used to create user-defined data types in Solidity.

The usage of an enum may help reduce errors due to invalid data assignments in the code.

For example, consider an application that sets the traffic signal lights, using enum it would be possible to restrict the inputs to Red, Orange, and Green. This will make sure that no one is able to set any other color to the traffic signal.

To use an enum, first, we need to define a new data type with the set of predefined values using the enum keyword. The syntax is:

enum name_of_data_type { predefined_values }

Then we need to create a variable for the new data type; the syntax is similar when using other variable data types.

enum name_of_data_type variable_name

Boolean

Solidity also provides a Boolean variable type, which can represent any scenario with a binary state. The possible values are true or false. The keyword used is bool. The default value of bool is false.

To create a boolean variable, the following syntax is used:

bool variable_name = value

Fixed-size Byte Arrays

Fixed-size byte arrays are used to hold a sequence of bytes from 1 to 32 using keywords bytes1, bytes2, bytes3, …, bytes32 correspondingly. Another keyword for bytes1 is byte.

The length member function can be used to get the length of a particular variable. Do keep in mind that it is a read-only property.

To create a bytes variable, we use the following syntax:

bytes32 variable_name = value

Implicit conversion from a string value to byte is possible. So it is possible to assign a string value to a byte variable in the program.

Fixed Point Numbers

The fixed-point number system or floating-point number system is used to store numbers that have decimal values. Solidity has not yet begun to start supporting fixed-point numbers due to the requirement for computation whenever they are included in an operation. As of now they can be declared but cannot be assigned to or from.

ufixedMxN and fixedMxN are reserved, where M denotes the number of bits taken by the type and N denotes how many decimal points are there. M should be divisible by 8 and ranges from 8 to 256. N ranges from 0 to 80 including 0 and 80.

Solidity Variables

Variables are reserved memory locations to store data. When a variable is declared, some space is reserved in memory for storing the data.

Solidity is a statically typed language, and hence, we need to specify the type of each variable declared. Variable types allow the compiler to make sure of the correct usage of each variable. Solidity has many primary data types (uint/int, bool, string, address, etc.), which can be further combined to form complex data types (struct, array, mapping).

Each declared variable always has a default value based on its type. There is no concept of "undefined" or "null".

The syntax for creating a variable is as follows:

data_type variable_name = value;

Example:

uint256 number = 123;

There are three types of variables in Solidity:

  • State Variables − Variables whose values are permanently stored in contract storage.
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

contract Storage {

    bytes32  msgValue; // State variable

    function store() public {
        msgValue  = "Hello Byte"; // Using State variable
    }
}
  • Local Variables − Variables whose values are present till the function is executing, and are stored in memory. Variables whose values are available only within a function where it is defined. Function parameters are always local to that function.
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

contract Storage {

    uint256 result; // State variable

    // function parameter(s) are  also local variable
    function add(uint256 number1) public {
        uint number2 = 1234;  // local variable

        result = number1 + number2;
    }

    function retrieve() public view returns (uint256){
        return result;
    }
}
  • Global Variables − Special variables that exist in the global namespace are used to get information about the blockchain.

These are special variables that exist in the global workspace and provide information about the blockchain and transaction properties. Some examples are:

  • msg.sender: returns an address, that of the sender of the message
  • timestamp: current block timestamp UNIX timestamp format. The complete list is available here.
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

contract Storage {

    function msgSender() public view returns(address){
        return msg.sender;
    }

    function getTime() public view returns(uint){
        return block.timestamp;
    }
}

In the above example, the msgSender function will return the address which has invoked that function. The function getTime will return the block timestamp.

Who can access a Contract?

Now let us discuss who can access a deployed smart contract's functions, variables, events, etc. Mainly there are four entities that are able to access a smart contract that is deployed on the Ethereum network. These are:

  • User - The one who controls an Externally Owned Account in Ethereum.
  • Self - The contract itself has access to all of its properties.
  • Derived Contract - The child contract of that particular contract has access.
  • External Contract - Finally external contracts can access the contract properties.

All of these actors do not have unrestricted access to the contract properties; this will be controlled by access specifiers or access modifiers.

Visibility/Access Modifiers

Scope of local variables is limited to the function in which they are defined, but for state variables it's different. Each state variable's scope visibility or access modifiers can be set; which decides who has access to these variables. The access modifiers available to state variables are:

  • public: The variable declared public will be accessible to everyone.
  • private: The variable declared private will only be available to the contract.
  • internal: A variable declared as internal will be accessible by the contract itself and by the child contract (instantiate a contract from within another contract). The default visibility of a state variable is internal. The table below provides a quick reference on how other entities can gain access based on the above access modifiers:
User Self External Contract Derived Contract
public
private
internal

Getter Functions: When you set a state variable as public the compiler will automatically create a getter function (function to get the data of the variable) using the name of the variable. Getter functions have external visibility.

The below example shows how to declare a variable as public, private, or internal. You won't be able to see any difference for the variables declared as private or internal but in the case of public, you will know it once you deploy it. Why don't you compile and deploy the contract to any Remix VM using the Remix IDE.

The syntax of declaring a variable with visibility/access modifier is as follows:

datatype visibility_modifier name_of_variable = value
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

contract ExampleStateVariable {
    uint public var1 = 123;
    uint private var2 = 456;
    uint internal var3 = 789;
}

Functions

A function is a block of code that accepts an arbitrary length of solidity types as input and provides an arbitrary length of solidity types as result. The function syntax is as follows:

function name () visibility_modifier {
}

The declaration starts by using the keyword function followed by the name of the function. You may follow the common naming conventions like: avoid using special symbols other than underscore or dash, don't start the function name with a number, can start using underscore etc. Function name and parameters are followed by the visibility modifier.

Example:

function store() public {
}

Here the function name is store, has public visibility which specifies who can access the function.

Function Input Parameters

A function may have zero or more input parameters. Function parameters can be declared in the same way as a state variable declaration, without the accessibility modifier. Function parameters can be used as any other local variables and they can also be assigned to.

The syntax for the function when input parameter comes into play is:

function name(parameter_type parameter_label) visibility_modifier {
}

Example:

function store(uint256 num) public {
}

When calling a function from other functions, the parameters can be passed as values or as labeled values. The below example depicts that property.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Storage {

    uint256 number;

    // function to store value to number variable
    function store(uint256 num) public {
        number = num;
    }

    // calling store function from another function
    function callStore() public {
        store(1001);
    }

    // calling store function from another function
    // by specifying input parameter name
    function callStoreAnother() public {
        store({num: 1001});
    }

    // function to retrieve the data in number variable
    function retrieve() public view returns (uint256){
        return number;
    }
}

Return Values

Just like function parameters, a function may have zero or more return values. In case a function does not have any return values, the entire return section is omitted. The syntax for the function with a return value is:

function name() visibility_modifier mutability returns(parameter_type parameter_label) {
}

The mutability specifies the function's nature of interaction with the blockchain state.

Example:

function retrieve() public view returns (uint256){
        return number;
}

Return values can be specified by specifying each return value data type and then using the return keyword to return the values. Another way is to label return values in the function header and then assign the result to that label. Both options are shown below:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Storage {

    uint256 number;

    // function to store value to number variable
    function store(uint256 num) public {
        number = num;
    }

    // function to retrieve the data in number variable
    function retrieve() public view returns (uint256){
        return number;
    }

    // function to retrieve the data in number variable
    // using output parameter label instead of return keyword
    function retrieveAnother() public view returns (uint256 _num){
        _num = number;
    }
}

In case you want to store the return value of the function to variable(s), that can be done as follows.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Storage {
    uint256 square;
    uint256 total;
    uint256 product;

    // a function to find square of a number
    // which has one output parameter
    function findSquare(uint256 number1) private pure returns (uint256 _total) {
        return number1 * number1;
    }

    // a function to find the total and Product of two numbers
    // has two output parameters
    function findTotalandProduct(uint256 number1, uint256 number2) private pure returns (uint256 _total, uint256 _product) {
        return (number1 + number2, number1 * number2);
    }

    // the function that calls and stores the data
    function storeValues(uint256 number1, uint256 number2) public {

        // storing the result of function findSquare() into variable
        square = findSquare(number1);

        // storing the result of function findTotalandProduct() into a tuple
        (total, product) = findTotalandProduct(number1, number2);
    }
}

Note: If you use a dynamic variable type as an input or output parameter you should use that type with the keyword memory.

Visibility/Access Specifiers

Functions also have visibility/access modifiers. Functions have four access modifiers:

  • public: A function declared as the public will be accessible to everyone.
  • private: A function declared as private will only be available to the contract itself.
  • internal: A function declared as internal will be accessible by the contract itself and by the child contract.
  • external: External functions are included in the contract's interface, enabling them to be accessed from other contracts and through external transactions, but they cannot be invoked within the contract itself.

Visibility types for functions defined in contracts have to be specified explicitly, they do not have a default. The below table provides a quick reference on the interaction of different entities to the contract based on the above access modifiers:

User Self External Contract Derived Contract
public
private
internal
external

The below example shows how to declare a function as public, private, internal, or external. At the moment, one can't immediately notice all the differences but the obvious ones can be found while deploying the below contract. Compile and deploy the contract to Remix VM using the Remix IDE.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract FunctionTest {
    uint var1;

    function getVar1() view external returns(uint) {
        return var1;
    }

    function callSetVar1(uint _var) public {
        setVar1(_var);
        setVar2(_var);
    }

    function setVar1(uint _var1) private {
        var1 = _var1;
    }

    function setVar2(uint _var1) internal {
        var1 = _var1;
    }
}

Once deployed, you may notice that the setVar1 function is not available in the remix interface. Since it is a private function, it can only be invoked within the smart contract and thus not available for external users.

Function Mutability Modifier

The mutability of a function means the extent of interaction of a function with the blockchain state. The function mutability is categorized into four:

  • default: Default mutability means the function can change the state of the blockchain and the function can read from the state of the blockchain. If no mutability is specified, a function is considered to be of default mutability.

  • view: The function can only view and can't modify the state. The following actions can be considered as modifying the state.

    • Writing to a state variable
    • Emitting events
    • Creating contracts
    • Using selfdestruct (with the exception of msg.sig and msg.data)
    • Sending Ether via calls
    • Calling any function not marked view or pure.
    • Using low-level calls.
    • Using inline assembly that contains certain opcodes.
  • pure: Almost the same as view, but pure function can't read or modify the state. Apart from the above list of state modifications, the pure functions can't perform the below actions:

    • Reading from state variables
    • Accessing address(this).balance or <address>.balance.
    • Accessing any of the members of the block, tx, msg (with the exception of msg.sig and msg.data).
    • Calling any function not marked pure.

pure and view functions do not make any changes to the blockchain, thus they do not have any cost for execution. So we do not need to worry about ether balance for calling pure/view functions.

  • payable: Function is able to receive ether on behalf of the contract.

The below example shows how to declare a function as pure, view, default, and payable. Let's compile and deploy the contract to Remix VM using the Remix IDE.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract FunctionMutability {
    uint number;

    function get() view public returns(uint) {
        return number;
    }

    function set(uint _number) public {
        number = _number;
    }

    function calculate(uint a, uint b) pure public returns(uint) {
        return a + b;
    }

    function sendEther() public payable returns(uint) {
        return address(this).balance;
    }
}

The get function returns the value of a state variable, and hence, it views the state. The calculate function is operating on the given input parameters only. Since it does not affect the state, it is tagged as pure. The sendEther function accepts Ether and thus it is marked as payable.

Custom Modifier

Function modifiers can be used to change the behavior of functions in a declarative way. This means you can use a modifier to check a condition prior to executing the function body or after the execution of the function body.

The syntax of the modifier is

modifier name(input parameters) {
    code block
    _;
}

The declaration starts with the modifier keyword followed by the name and then the input parameters. The _; (underscore semicolon) is used to indicate where the execution of the function body should start; it can either be at the beginning or after the modifier code is executed.

Let us write a modifier that will check an integer input value, and if the input value is greater than five, it will let the function execute; otherwise, entirely revert the transaction that triggered the function.

First, let us write a function called isItGreaterthanFive, which will take an integer as input and simply return a true value if nothing else happens.

function isItGreaterThanFive(int value) pure public returns(bool) {
    return true;
}

Now let us write a modifier named yesItIs that receives an integer input and checks if the value is greater than five.

modifier yesItIs(int _value) {
    _;
}

Next, we write an if condition that checks if the value is greater than five or not, and revert the transaction if it fails. We can use an inbuilt function named revert() which takes in a string value as input. Basically we can provide the reason for revert in that string value. So let's update our modifier.

modifier yesItIs(int _value) {
    if (_value < 5) revert("Input not greater than five");
    _;
}

Let us integrate this into our function.

function isItGreaterThanFive(int value) pure public yesItIs(value) returns(bool) {
    return true;
}

Try to execute this contract in Remix IDE and check the result if we give a value less than 5. Full contract code is given below.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Test {

    modifier yesItIs(int _value) {
        if (_value < 5) revert("Input not greater than five");
        _;
    }

    function isItGreaterThanFive(int value) pure public yesItIs(value) returns(bool) {
        return true;
    }
}

Control Structures

Solidity provides the features of control structures, which helps us to control the flow of smart contract execution. However, some control structures like switch are not available in Solidity. Below is the list of available control structures in Solidity.

  • if Statement
  • while Loop
  • do... while Loop
  • for Loop
  • Loop Controllers

Let's look into these in detail.

if

When we want to execute a particular block of code based on certain condition we can use if. if is a control structure that can be used to check for a condition and depending on the result of the condition, being true, we can execute a block of codes. The syntax is:

if (condition) {
      // execute the code
}

When we want to execute another code block based on the fact the condition in if being false, we can use the else block. else will execute when the condition evaluates to false. The syntax will be as follows:

if (condition) {
      // execute the code
} else {
      // execute the code
}

Let's look at an example, that demonstrates the use of the if...else control structure. The program aims to find out the largest of the two numbers.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyContract {

    function largest(uint a, uint b) public pure returns(uint max){
        if(a > b)
            max = a;
        else
            max = b;
    }

}

The 'if...else if...' statement is an advanced form of 'if...else' that allows Solidity to make a correct decision out of several conditions. The syntax is as follows:

if (condition) {
      // execute the code
} else if (condition) {
      // execute the code
} else  {
      // execute the code
}

Now let's look at the example below, which aims to find the largest of the three numbers.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyContract {

    function largest(uint a, uint b, uint c) public pure returns (uint max) {
       if (a > b && a > c) {
           max = a;
       }
       else if(b > c) {
           max = b;
       }
       else {
           max = c;
       }
    }

}

In both examples given above, we do not require the return keyword to return the result. This is because we have labelled the function output parameter and hence it will be treated just like a local variable. Though return keyword is not specified, the labelled parameter (here, it's max) will be considered as output.

The conditional ternary operator assigns a value based on a particular condition, this can be used in place of if statement, the syntax is

If Condition is true? Then value is X : Otherwise value is Y

Example:

max = (a>b) ? a : b;

Let's see an example that aims to find the biggest of two numbers, but this time we use the ternary operator instead of if...else.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyContract {

    function greatest(uint a, uint b) public pure returns(uint max){
       max = (a>b) ? a : b;
    }

}

for

The for loop in Solidity is used to iteratively execute a code block as long as a condition holds. The syntax is

for (initialize counter; condition check; update counter) {
         // code block to be executed
}

The initialize counter is executed before the execution of the code block, then we have a condition check that defines the condition for the code block to be executed, and finally, update counter that executes every time after the code block is executed.

Let us check an example of a for loop that finds the factorial of the given number.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyContract {

    function factorial(uint num) public pure returns(uint fact){
        uint f = 1;
        for(uint i = 1; i <= num; i++) {
            f = f * i;
        }
        fact = f;
    }
}

while

The while loop will repeatedly execute a set of code as long as the condition is true. The syntax is

while (condition) {
    // execute the code
}

Now let's look at the below example, which uses a while loop to find the sum of digits in a number.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyContract {
    function sumOfDigits (uint num) public pure returns(uint s){
       uint remainder;
       uint sum;
       while(num > 0){
           remainder = num % 10;
           sum += remainder;
           num = num / 10;
       }
       s = sum;
    }
}

During each iteration of the loop, we get the last digit(remainder) of the current number (num) add it to previous last digit and remove the last digit of the number(num) by dividing it with 10 and ignoring the decimal. The process is continued until all the digits of the number is extracted.

The while loop and for loop are also known as the entry control loop as the condition is evaluated before processing a body of the loop.

do...while

The do...while loop also executes a set of codes until the condition turns out to be false, but the difference from the while loop is that the condition check happens after executing the loop body, and due to this fact do...while loop is known as an exit control loop. The syntax is

do {
      // execute code
} while (condition);

Let's look at the same example of finding the sum of digits in a number, but this time using the do...while loop.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyContract {
    function sumOfDigits (uint num) public pure returns (uint s){
       uint remainder;
       uint sum;
       do
       {
           remainder = num % 10;
           sum += remainder;
           num = num / 10;
       } while(num > 0);
       s = sum;
    }
}

Loop control

Similar to how we use loops to control the flow of smart contract execution, we can control the loops' execution flow up to some limit using continue and break.

  • continue - which is used to skip the current iteration in the loop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyContract {
    function findSum() public pure returns(uint sum){
        for(uint i = 1; i <= 5; i++) {
            if (i == 1) {
                continue;
            }
            sum = sum + i;
        }
    }
}

Here we are using a continue conditionally inside the loop. If the value of i is 1, the following statement is skipped and execution continues from next iteration of the loop.

  • break - will exit the loop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract MyContract {
    function findSum() public pure returns(uint sum){
        for(uint i = 1; i <= 5; i++) {
            if (i == 5) {
                break;
            }
            sum = sum + i;
        }
    }
}

Here we are using a break conditionally inside the loop. If the value of i is 5, the remaining loop execution is skipped and execution resumes from statement after the loop.

Arrays

Arrays are used to group together variables of the same type, in which each individual variable has an index location using which it can be retrieved. The array size can be fixed or dynamic.

To create a fixed-sized array, the following syntax is used.

Type[size] array name

Example:

unit[8] numbers;

To create a dynamic array, just leave the size field blank.

Type[] array name

Example:

unit[] numbers;

Member Functions

  • length: can be used to get the size of both fixed and dynamic array.
  • push: is used to insert elements to a dynamic array.
  • pop: is used to remove the last element of a dynamic array. This also implicitly calls delete on that element. The below example depicts the usage of arrays and the member functions. Go ahead and run it in Remix IDE.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract FixedSize {
    uint[8] public numbers = [1, 2];

    function insert(uint value, uint index) public {
        numbers[index] = value;
    }

    function returnLength() view public returns(uint) {
        return numbers.length;
    }
}

contract DynamicSize {
    uint[] public numbers = [1, 2, 3, 5];

    function insert(uint x) public {
        numbers.push(x);
    }

    function remove() public {
        numbers.pop();
    }

    function returnLength() view public returns(uint) {
        return numbers.length;
    }
}

Strings and Bytes

The string and bytes variable types are special arrays. The bytes behave similarly to the byte[] dynamic array and are used to store arbitrary-length raw byte data. Bytes support push, pop, and length. Strings are the same as bytes and are used to store arbitrary-length of string data of the format UTF-8, but do not support length or index access. In Solidity, there are no string manipulation functions inbuilt, but if necessary, many third-party libraries are available for it.

Memory Arrays

It is possible to create dynamic memory arrays in Solidity. If needed the lengths of the dynamic arrays can be specified using the new operator.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Dynamic {
    bytes[] b = new bytes[](7);

    function test(uint len) public {
        b = new bytes[](len);

        string memory st = new string(len);
        bytes memory by = new bytes(len);
    }
}

Structs

Struct allows us to group the different or the same type of variables together to form user-defined data types.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract TestStruct {
   struct Book {
        uint id;
        string title;
        string author;
   }

   Book book;

   function setBook() public {
        book = Book(1001, 'Learn Blockchain Part 1', 'KBA');
   }

   function setBookAnother() public {
        book.id = 1002;
        book.title = 'Learn Blockchain Part 2';
        book.author = 'KBA';
   }

   function getBookDetails() public view returns (uint, string memory, string memory) {
        return (book.id, book.title, book.author);
   }
}

Here, we have created a struct named Book with id of integer type, title, and author of string type. We have two ways to set values to a struct. In the function setBook() we create an instance of a struct with the values we want to insert; keep in mind that the values should be in the order they are defined in the struct, and values to all members are mandatory.

In the second function setBookAnother() we are independently assigning values to each member variable in the struct, here we don't need to follow an order and don't need to assign values to all members at once. The values of a struct can be returned as individual members; passing an entire struct is not currently supported.

Nested Struct

We can nest the structs together, let's look at the below example.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract TestStruct {
    // Define a struct to hold KYC information
    struct kyc {
        uint256 iD;
        string firstName;
        string lastName;
        AddressDetails location;
    }

    // Define a nested struct to hold address details
    struct AddressDetails {
        string buildingName;
        string street;
        string state;
        uint256 pinCode;
    }

    // Create a public variable of type kyc to store customer information
    kyc public customer;

    // Function to create a new customer with KYC details
    function newCustomer(
        uint256 _iD,
        string memory _firstName,
        string memory _lastName,
        string memory _buildingName,
        string memory _street,
        string memory _state,
        uint256 _pinCode
    ) public {
        // Create a new kyc struct instance with provided details
        customer = kyc(
            _iD,
            _firstName,
            _lastName,
            AddressDetails(_buildingName, _street, _state, _pinCode)
        );
    }

    // Function to update customer details (less efficient, prefer newCustomer)
    function _newCustomer(
        uint256 _iD,
        string memory _firstName,
        string memory _lastName,
        string memory _buildingName,
        string memory _street,
        string memory _state,
        uint256 _pinCode
    ) public {
        // Update individual fields of the customer kyc struct
        customer.iD = _iD;
        customer.firstName = _firstName;
        customer.lastName = _lastName;
        customer.location.buildingName = _buildingName;
        customer.location.street = _street;
        customer.location.state = _state;
        customer.location.pinCode = _pinCode;
    }

    // Function to retrieve complete customer KYC details
    function getCustomerDetails() public view returns (kyc memory) {
        // Return the entire customer kyc struct
        return customer;
    }

    // Function to retrieve customer details individually (less efficient, prefer getCustomerDetails)
    function _getCustomerDetails()
        public
        view
        returns (
            uint256,
            string memory,
            string memory,
            string memory,
            string memory,
            string memory,
            uint256
        )
    {
        // Return individual fields of the customer kyc struct
        return (
            customer.iD,
            customer.firstName,
            customer.lastName,
            customer.location.buildingName,
            customer.location.street,
            customer.location.state,
            customer.location.pinCode
        );
    }
}

Data Location

The reference type has an extra annotation to specify the data location,specifying where the assigned value is stored. The data locations are:

  • Memory
  • Storage
  • Calldata

Calldata is similar to memory, it is non-modifiable, non-persistent, and is used to store function arguments. Before Solidity version 0.5.0, there was no need to explicitly specify the data location for the reference type variables.

Data Location and Assignment Behavior

  • Assignments between storage and memory (or calldata) always create independent copies.
  • Assignments from memory to memory only create references. Updates in one place is reflected in another because all the variables point to the same data location.
  • Assignments from storage to a local storage variable also assign references.
  • All other assignments to storage always create copies.

Mapping

The mapping type is a key-value data structure, which is similar to the hash table. A hash table initializes every possible key with a value whose byte representation is all zero, which is usually a type's default value. But in the mapping, the key is not stored, instead, the hash value of the key is stored along with the value.

As mapping does not store keys and has no length property, it requires the assigned keys to get updated. Mappings can only be located in storage. If mappings are used in any struct or array the same rules are applied to those instances also.

The syntax for mapping is:

mapping (key => value) name_of_mapping

The mapping is declared with the keyword mapping and each key will have a corresponding value. Key and value can be any of the datatypes that you want to map.

In DApp, most of the data are stored with a combination of mapping and struct.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract TestMapping {
   struct Book {
        uint id;
        string title;
        string author;
   }

   mapping(uint => Book) books;

   function setBook() public {
        books[1001] = Book(1001, 'Learn Ethereum Part 1', 'KBA');
   }

   function setBookAnother() public {
        books[1002].id = 1002;
        books[1002].title = 'Learn Ethereum Part 2';
        books[1002].author = 'KBA';
   }

   function getBookDetails() public view returns (uint, string memory, string memory) {
        return (books[1001].id, books[1001].title, books[1001].author);
   }
}

If you mark a state variable of the type mapping as public, a getter function is created for the mapping where the key will be the getter function's argument. If the value is of type struct, then it gets returned. In case it's an array or another mapping, then the function will have one more parameter for each key.

If you create a mapping inside another mapping, declare another mapping for the value part, as shown in the program below.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract TestMapping {
   mapping(uint => mapping(uint => string)) public studentsName;

   function setName(uint batchNo, uint rollNo, string memory studentName) public {
       studentsName[batchNo][rollNo] = studentName;
   }
}

The below table depicts the suitability of Solidity types as key and value.

Data type Key Value
uint
int
bool
string
bytes
enum
address
contract
array
struct
mapping

Iterable Mappings

One cannot iterate over mapping due to the fact that mappings are not sequential. But it is possible to do so if one build a data structure using mapping and iterate over that. For example, if we want to make the example for mapping struct, we can simply implement a count variable and use it as a key for the mapping. Then we can iterate over mapping.

The example below depicts this.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract IterableMapping {

    // Define a struct to hold book information
    struct Book {
        uint256 id;
        string title;
        string author;
    }

    // Create a mapping to store books with unique identifiers (uint) as keys
    mapping(uint256 => Book) public books; // Specify public visibility for readability

    // Variable to track the number of books added (acts as a counter)
    uint256 public bookCount = 0; // Use a more descriptive name

    // Function to add a new book with predefined details
    function setBook() public {
        // Add a new book to the mapping using bookCount as the key
        books[bookCount] = Book(1001, "Learn Blockchain Part 1", "KBA");
        // Increment bookCount after adding the book
        bookCount += 1;
    }

    // Function to add another book with separate details
    function setBookAnother() public {
        // Add a new book using bookCount as the key and update details individually
        books[bookCount].id = 1002;
        books[bookCount].title = "Learn Blockchain Part 2";
        books[bookCount].author = "KBA";
        // Increment bookCount after adding the book
        bookCount += 1;
    }

    // Function to retrieve details of a specific book based on index
    function getBookDetails(uint256 index)
        public
        view
        returns (
            uint256,
            string memory,
            string memory
        )
    {
        // Return details of the book at the provided index (be cautious of out-of-bounds access)
        return (books[index].id, books[index].title, books[index].author);
    }
}


Clone this wiki locally