-
Notifications
You must be signed in to change notification settings - Fork 44
Solidity: Basics
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.
- Supports inheritance including multiple inheritances with C3 linearization.
- Supports libraries
- Supports complex user-defined data types like mapping and struct.
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.
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
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. Keywordsuint8
touint256
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. Keywordsint8
toint256
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
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
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 theaddress
but with additional member functionstransfer
andsend
. Basically, we can send ether toaddress payable
but not toaddress
.
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 >
-
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 automaticallyrevert
the transaction. The transfer has a low-level counterpart known assend
, which does the same but will not revert the transaction, instead will return a false value upon failure.
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
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 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.
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.
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.
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.
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 isinternal
. 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;
}
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.
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;
}
}
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
.
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.
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
andmsg.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
andmsg.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.
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;
}
}
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.
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;
}
}
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;
}
}
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.
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;
}
}
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 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;
-
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;
}
}
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.
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);
}
}
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.
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
);
}
}
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.
- 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.
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 |
❌ | ✅ |
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);
}
}
- Introduction
- Rise of Ethereum
- Ethereum Fundamentals
- DApps & Smart Contracts
- MetaMask Wallet & Ether
- Solidity: Basics
- Solidity: Advanced
- Solidity Use cases and Examples
- DApp Development: Introduction
- DApp Development: Contract
- DApp Development: Hardhat
- DApp Development: Server‐side Communication
- DApp Development: Client-side Communication
- Advanced DApp Concepts: Infura
- Advanced DApp Concepts: WalletConnect
- Event‐driven Testing
- Interacting with the Ethereum Network
- Tokens: Introduction
- Solidity: Best Practises
- Smart Contract Audit
- Ethereum: Advanced Concepts
- Evolution of Ethereum
- Conclusion