From Zero to Hero with Solidity

If you want to get into Web3 you should know how to program a Smart Contract. This article will teach you everything you need to know about the Solidity programming language. By working your way through this article you will write multiple Smart Contracts.

What is a Smart Contract?

Nick Szabo coined the term Smart Contract in 1997. If you are familiar with javascript then you know the term programs. A smart contract is the name for programs written on the Ethereum blockchain.

Every block in the blockchain contains an Ethereum virtual machine (EVM). When you write a smart contract it is executed by the EVM.

What is Solidity?

You may be familiar with Javascript as a programming language. Solidity is the programming language used on Ethereum.

Solidity is an object-oriented, high-level language that is influenced by C++, Python, and JavaScript.

Some of the features of Solidity are:

  • statically typed
  • support inheritance
  • supports libraries
  • allows for complex user-defined types

You can find more details about Solidity in their documentation.

What is the Remix IDE?

Remix IDE is a powerful open-source tool that helps you write Solidity contracts straight from the browser.

It is written in JavaScript and supports both usages in the browser, but runs locally and in a desktop version.

Remix IDE has modules for testing, debugging, and deploying of smart contracts and much more.

You can read the Remix IDE documentation here.

Intro to the Remix IDE

Remix-IDE is available at remix.ethereum.org. Go there to get to the IDE.

remix ide layout

  1. Icon Panel - click to change which plugin appears in the Side Panel
  2. Side Panel - Most but not all plugins will have their GUI here.
  3. Main Panel - In the old layout, this was just for editing files. In the tabs can be plugins or files for the IDE to compile.
  4. Terminal - where you will see the results of your interactions with the GUIs. Also, you can run scripts here.

Icon Panel at Page Load

When you load remix - the icon panel shows these icons by default.

remix icons

Themes

So you want to work on Remix with a dark theme or a gray theme or just a different theme that the one you are currently looking at? Go to the settings tab and at the bottom is a choice of lots of Bootstrap-based themes.

remix themes

File Explorers

To get to the File Explorers module - click the file explorers icon.

remix file explorers

File Storage

By default, Remix IDE stores files in Workspaces which are folders in your browser’s local storage.

Important Note: Clearing the browser storage will permanently delete all the files stored there.

Workspaces

Workspaces are new as of Remix IDE v0.11.0. Workspaces should help clean up & organize your Files Explorer by allowing you to separate your projects.

The Files Explorer’s Workspaces all have a contracts folder, a scripts folder, a tests folder, and a README.txt.

Files Explorer Tour

remix file explorers

We will start by reviewing the icons in the image above.

The book icon - A. is the link to the documentation (this page).

Workspace Manipulation

Add a Workspace B. Rename a Workspace C. Delete a Workspace D.

File Manipulation

Click on the new file icon (E.) and an input for a new file’s name will appear in the Explorer. Once a name is entered, the new file will open in the Editor. This file will be empty.

file manipulation

When you click on the new file icon, the new file will be placed in the currently selected folder. If a file and not a folder is selected then the new file will be placed in that file’s folder. And if nothing is selected, then the file will be placed in the root of the current workspace’s folder. Or to be brief — just be mindful of what folder it lands in.

Create a folder

The icon (marked F. above) creates a new folder in the current workspace.

Publish to Gist

The icon (marked G. above) publishes all files from the current Workspace to a gist. Only files in the root of the browser will be published. Files in subfolders will not be published to the Gist.

Gist API requires users to be authenticated to be able to publish a gist.

Click this link to Github tokens setup and select Generate new token. Then check the Create gists checkbox and generate a new token. Also, make sure you check the box to enable the creation of Gists with this token.

Take the token and paste it in Remix’s Settings module in the Github Access Token section. And then click Save. For the moment, after saving, in order for the token to be registered, you will need to refresh Remix. In an upcoming release, it will not be necessary to do the refresh.

Upload to Browser Storage

Click the icon marked H. to upload a file from your computer’s file system to your browser’s local storage.

Change Workspaces

Click on the icon marked I. to change to a new workspace.

Create Your First File

Make sure File Explorers is shown in the side panel. If not, click the icon in the icon panel to show it.

Click on the new file icon and create a new file titled HelloWorld.sol.

Layout of a Solidity Source File

Source files can contain an arbitrary number of contract definitions, import directives, pragma directives and struct, enum, function, error and constant variable definitions.

SPDX License Identifier

Trust in smart contracts can be better established if their source code is available. Since making source code available always touches on legal problems with regard to copyright, the Solidity compiler encourages the use of machine-readable SPDX license identifiers. Every source file should start with a comment indicating its license.

In your HelloWorld.sol file add this as the first line:

// SPDX-License-Identifier: MIT

Pragmas

The pragma keyword is used to enable certain compiler features or checks. A pragma directive is always local to a source file, so you have to add the pragma to all your files if you want to enable it in your whole project. If you import another file, the pragma from that file does not automatically apply to the importing file.

Version Pragma

Source files can (and should) be annotated with a version pragma to reject compilation with future compiler versions that might introduce incompatible changes.

At the time this course was written, the latest version of the Solidity Compiler was version 0.8.12. Note these versions are updated on a regular basis. For this course, we will be using version 0.8.12.

Add this pragma version as the second line in your smart contract:

pragma solidity ^0.8.12;

Comments

Single-line comments (//) and multi-line comments (/*...*/) are possible.

<span class="hljs-comment">// This is a single-line comment.</span>
\*
This is a
multi-line comment.
*/

Next, add a comment providing details about our smart contract. You can put whatever you want. For my contract I am adding this comment:

// My first smart contract

Naming Your Contract

The rule of thumb in naming your contract is that it has the same name as the filename. Our filename is HelloWorld.sol. Our contract will be called HelloWorld

Under your comment create your Smart Contract with these lines:

contract HelloWorld</span> {

}

A contract is defined by the user of the word contract followed by the name of the contract. The contents of the contract are enclosed in squiggly brackets - {}.

Structure of a Contract

Contracts in Solidity are similar to classes in object-oriented languages. Each contract can contain declarations of State Variables, Functions, Function Modifiers, Events, Errors, Struct Types, and Enum Types. Furthermore, contracts can inherit from other contracts.

That sounds like a lot but don't worry. By the end of this course, we will cover all those items. First, let's start with state variables.

State Variables

State variables are variables whose values are permanently stored in contract storage.

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. Some examples of variable types are:

Booleans

bool: The possible values are constants true and false

Integers

int/uint: Signed and unsigned integers of various sizes

Fixed Point Numbers

fixed/ufixed: Signed and unsigned fixed-point numbers of various sizes

Address

The address type comes in two flavors, which are largely identical:

  • address: Holds a 20-byte value (size of an Ethereum address)
  • address payable: Same as address, but with additional members transfer and send

Strings

string: String literals are written with either double or single quotes. It can only contain ASCII.

Adding our first State Variable

Inside your contract let's define a string variable called myPhrase. Assign a value to the string. I am assigning my first name to the string so you do the same for your string. It should look like this:

string myPhrase = "Jennifer";

By default, this string is private. This determines the visibility of the state variable. Variables can be declared as:

  • public - any contract and account can access
  • private - access only inside the contract that defines the variable
  • internal - accessed from the contract that defines the variable as well as child contracts but not from external contracts

Public vs Private

For now, let's leave our string private. Click on the Solidity Compiler icon in the icon bar.

compiler icon

In the sidebar, you will see a button to compile your HelloWorld.sol smart contract.

compile button

Click the button to compile your project. If everything is compiled successfully you will see a green check button next to the Solidity Compiler icon.

successful compile

Click on the deploy & run transactions button in the icon bar.

deploy button

Make sure the contract shown is your HelloWorld contract. If it is showing a different contract, click on the dropdown button and select your HelloWorld contract.

Click on the Deploy button.

At bottom of the sidebar, you will have a section titled Deployed Contracts. You will see your HelloWorld contract. Click on the arrow next to the name of your contract.

Expanding the entry for your contract it will show all state variables and functions that are publically visible. As you can see in the image below, nothing is shown.

deployed contract

We don't see anything because our state variable myPhrase is private. Go back to your contract and change it to be a public variable like this:

string public myPhrase = "Jennifer";

Then go through the same steps we did previously and compile your smart contract and then deploy it.

Now when you expand your smart contract you should see a button that has the name of our public state variable. If you click on the button it will show you the type of the variable (string) as well as the value of the variable (Jennifer).

contract

Constructor

A constructor is an optional function declared with the constructor keyword which is executed upon contract creation, and where you can run contract initialization code.

A constructor is optional. Only one constructor is allowed, which means overloading is not supported.

Before the constructor code is executed, state variables are initialized to their specified value if you initialize them inline, or their default value if you do not.

There are a couple of things I want us to change in our HelloWorld contract. They are:

  • make the variable myPhrase to be private
  • do not assign a default value to the variable
  • Set the value of the variable in the constructor function
  • write a function that returns the current value of the myPhrase variable
  • write a function that set the value of the myPhrase variable to the value passed into the function

So let's get started.

Make variable private

Remember, I said that the default visibility of a variable is private. You do not have to use the keyword private in front of the variable name unless you want to. Currently, our variable is public. Change it by removing the keyword public. It should look like this:

string myPhrase = "Jennifer";

Don't assign default value

Currently, our private variable is assigned a value of "Jennifer". We want to remove that default value because in the next step we will be assigning the value in the constructor.

Remove the value from the variable so it looks like this:

string myPhrase;

Add a Constructor function

Below the declaration of our variable, we will add our constructor function. The format of a constructor function is:

constructor() {
  // you code goes here
}

You can pass in values to the constructor function that you can use inside the constructor.

When you deploy your HelloWorld smart contract, you can specify a value for the myPhrase variable. We will set the value of myPhrase to this value.

To do that we have to add a parameter to the constructor function. This variable will be of type string and I will call the variable _startPhase.

The general rule of thumb is variables passed into functions start with _ in their name.

Saving a state variable to the blockchain uses gas. That means it costs money. Since we don't want to save the value of the _startPhase variable we can add a modifier called view. Here is what our constructor function should look like:

constructor (string memory _startPhrase) {

}

The last thing we need to do in this step is set the value of myPhrase to _startPhrase. Our completed constructor should look like this:

constructor(string _startPhrase) {
    myPhrase = _startPhrase;
}

Getter Function

When we created a public state variable, Solidity automatically added a getter function that would show the value of the variable. But what if our state variable was private? In that case, we would have to create our function to get the value of the variable as well as another function that would set the value of the variable.

Below the constructor, we are going to create our first function. We will name this function getPhrase.

The basic format of a function in Solidity looks like this:

function() {
    // your code goes here 
}
`

You can add modifiers to functions just like we did with our state variable when we added the modifier public. The modifier order for a function should be:

  1. Visibility
  2. Mutability
  3. Virtual
  4. Override
  5. Custom modifiers

We want our function to be visible to everyone so we need to add the modifier public.

function getPhrase() {
    // your code goes here
}

The next modifier we want to add is for mutability. Functions can be declared view in which case they promise not to modify state. Getter methods are automatically marked view.

function public view() {
    // your code goes here
}

Our function will return the value of myPhrase so we have to define this. This declaration is done by adding the modifier returns and then specifying the value that is returned.

function  getPhrase  public  view  return  (string ) {
    // your code goes here 
}

We do not want to store the value returned from the getPhrase function. If we did store it then it would cost us money in terms of paying for it with the gas price. Just like we did with our constructor function we can add the modifier memory so that the value is not stored. Thus we don't incur the gas charge.

Our function now looks like this:

function  getPhrase  public  view  return  (string  memory) {
    // your code goes here 
}

The last thing we need to do is to return the value of myPhrase. Our final function looks like this:

function getPhrase  public view  return  (string)  {
   return  myPhrase;
}

Test our smart contract

Follow the steps that we have done previously and compile your smart contract. After it has compiled successfully you want to deploy it.

Next to the deploy button is an input field where you can put the value you want to have for the _startPhrase variable. Enter in a valid string value in this input field as shown in the image below:

deploy with phrase

After your smart contract has been deployed you can expand it and see a button called getPhrase. This button will execute our public function that returns the value of the myPhrase variable.

Click on the button and you will see the value like this:

deployed contract

Write a setter function

The last step we want to do is to be able to change the value of the myPhrase variable. To do this we will add a new function that I am going to call setPhrase. The start of our function looks like this:

function setPhrase()   {
    // your code goes here 
}

We want our function to have public visibility so we need to add the public modifier to our function.

function setPhrase() public   {
    // your code goes here 
}

Just like our constructor function, this function will have a value passed into it. This value will be of type string.

We don't want to store the value passed into the function which would incur gas fees. Just like the constructor function, we will add the memory modifier.

Lastly, we have to provide the name of the variable that will be passed in. I am going to call it _newPhrase. Our update function looks like this now:

function setPhrase (string memory _newPhrase )  public  {
    // your code goes here 
}

The last thing we need to do is update the value of myPhrase with the value of _newPhrase. Our final function looks like this:

function setPhrase ()  public  {
  myPhrase = _newPhrase ;
}

Now you can compile your function. Once it compiles successfully you can deploy by specifying the initial value. In my case, I am going to use Hello World.

Expand your contract and you will see two buttons. One is setPhrase and the other is getPhrase.

Click on getPhrase and you will the value Hello World (or whatever value you used when you deployed your contract).

Next to the setPhrase button is an input field where you can put in a new value. In my case, I put in Jennifer.

After entering the value click the setPhrase button.

You can verify the myPhrase variable was updated by clicking on the getPhrase button. You should now see the new value you had entered.

Challenge Section

The Challenge Section is where you can elect to do additional work that is not defined in this project. The goal of the challenge sections is to allow you to do more in-depth work with smart contracts.

Here are some challenges you may elect to undertake:

  1. Add a new state variable that will contain a person's age. Set the default value to your age. Then write functions that will set and get the person's age.
  2. Write a function that will return the phrase and the age in a single sentence.
  3. When you deploy the function, allow the user to specify a number. When you run this function it will return if the person's age is above, below or the same as the number entered.

Contract Using Variables

Previously we learned about the different value type state variables. We are going to create a contract that allows us to implement these variable types as well as improve our skills in writing functions in Solidity.

Create a new Contract File

Make sure File Explorers is shown in the side panel. If not, click the icon in the icon panel to show it.

Click on the new file icon and create a new file titled VariableDemo.sol.

For the first two lines in our contract, we will add our SPDX-License-Identifier and a pragma statement with our version of solidity. It should look like this:

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

Next, we will define our contract. Following the rule-of-thumb for naming contracts, it will be named the same as our filename like this:

contract VariableDemo {
}

The goal of this contract is to get first-hand experience using the different types of state variables that are available in Solidity. Let's define four state variables - a boolean, a uint256, an address and finally a string. Here is what it looks like:

contract VariableDemo {
    // Set our variables
    bool public truthy;
    uint8 public myInt;
    address public myAddress;
    string public myString;
}

Dealing with Boolean variables

When we created our boolean variable truthy we declared it to be public. Solidity by default will generate a getter function for us to display the value of the boolean.

If we want to change the value of the boolean then we would need to write a setter function. Let's create a function called truthyChange that will allow a user to pass a value of either true or false. We will set the variable truthy to that value.

Here is what the function looks like:

// Change a boolean
function truthyChange(bool _truthy) public {
    truthy = _truthy;
}

You can test out your smart contact so far by compiling it.

After you compile your function, you can deploy it. In your deployed smart contract, you will see the getter function provided by default by Solidity as well as our function truthyChange. Test it out by getting the value of the boolean variable. Then change the value of the boolean. Get the value of the boolean variable to verify that the value was changed.

Challenge Section What do you think happens if you put something other than true or false for the value of the truthyChange function? Does the function run?

When we defined our public variable truthy we did not initially give it a value. What is the default value that Solidity assigned to a boolean?

Dealing with Integer variables

We defined a public variable called myInt. It is an unsigned integer. Just like our boolean variable we do not initially assign any value to this variable.

Let's write a function called setMyInt that will set the value for this variable. If you want I would encourage you to try and write this function before viewing what I will give you below. Trying something on your own is the best way to learn how to program in Solidity.

Here is the setMyInt function:

// Change an integer
function setMyInt(uint8 _myInt) public {
    myInt = _myInt;
}

Test out this new function. Compile your contract and deploy it. Get the default value for myInt variable. Assign a new value to the variable. Check the value for the variable to verify it changed.

Let's add two more functions to handle our uint8 variable. If you are familiar with JavaScript then you are familiar with the shorthand way to either increment or decrement an integer by 1.

Let's add two functions to increment and decrement the value of the myInt variable by one. The two functions will be called incrementMyInt and decrementMyInt.

Without looking at the code I will provide below, I encourage you to try and write the functions yourself first.

Here are the two functions:

// Increase an integer
function incrementMyInt() public {
    myInt++;
}
// Decrease an integer
function decrementMyInt() public {
    myInt--;
}

Challenge Section

What was the default value Solidity assigned to an unsigned integer variable?

An uint8 integer must contain a value that is either equal to zero or greater than zero and must be less than 2**8 - 1 or 256.

Change the incrementMyInt function so that it will not allow you to increment the value above 256.

Change the decrementMyInt function so that it will not allow you to decrement the value of the variable to be below zero.

Dealing with Address variables

We have not covered the address variable in much detail yet. An address variable contains an Ethereum address. Once we have an address we can get the balance that address has.

In the Deploy & Run Transactions tab in Remix, you will see a dropdown field called account. By default, Remix will provide you with 10 different accounts that you can use in testing your contract. Each account is assigned 100 Eth by default.

When you interact with a smart contract, you have an Ethereum address. Solidity provides a default value called msg.sender which contains the Ethereum address of the user interacting with the contract.

We will use this knowledge to set the value of the address variable myAddress to the value of msg.sender. Once the value is set we can write a function called getMyBalance that returns the Eth balance.

Let's first write the function setMyAddress. It looks like this:

// Set the address to the sender of the contract
function setMyAddress() public {
    myAddress = msg.sender;
}

Now that we have set the address, we can write the function getMyBalance. See if you can write this function yourself and then check your code with what is shown below:

// Get the balance of an address
function getMyBalance() public view returns (uint256) {
    return myAddress.balance;
}

You can test out both of these functions. Does it show the correct balance of your account?

Dealing with String variables

We have covered string variables in the first contract we created. In that contract, we wrote functions to set the value of a string as well as get the value of the string.

Here are the two functions for the string variable:

// Change the value of a string
function setString (string memory newString) public {
    myString = newString;
}
// Get the value of the string
function getMyString () public view returns (string memory) {
    return myString;
}

Now you have written your second smart contract in Solidity. You are familiar with value-type state variables.

Types

Solidity is a statically typed language, which means that the type of each variable (state and local) needs to be specified.

The concept of “undefined” or “null” values does not exist in Solidity, but newly declared variables always have a default value depending on their type.

Types in Solidity are:

  • value types
  • reference types
  • mapping types

Value Types

In the previous section, I covered value types when we learned about state variables. Here is a refresher.

The following types are 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.

Some examples of value types in Solidity are:

Booleans

bool: the possible values are constants true and false.

Integers

int/uint: Signed and unsigned integers of various sizes

Fixed Point Numbers

fixed/ufixed: Signed and unsigned fixed-point numbers of various sizes.

Address

The address type comes in two flavors, which are largely identical:

  • address: Holds a 20-byte value (size of an Ethereum address)
  • address payable: Same as address but with the additional members transfer and send

Reference Types

Values of a reference type can be modified through multiple different names. Contrast this with value types where you get an independent copy whenever a variable of value type is used. Because of that, reference types have to be handled more carefully than value types. Currently, reference types comprise structs, arrays and mappings. If you use a reference type, you always have to explicitly provide the data area where the type is stored: memory (whose life is limited to an external function call), storage (the location where the state variables are stored, where the lifetime is limited to the lifetime of a contract) or calldata (special data location that contains the function arguments).

In this section, we will be covering all three of these reference types.

Arrays

An Array is a data structure, which stores a fixed-size sequential collection of elements of the same type. It is important to emphasize the last part of that sentence: an array is a collection of variables of the same type. Array elements can be of any type, including mapping or struct.

In the last section, we learned how Solidity will automatically create a getter function for public state variables. It is possible to mark state variable arrays public and have Solidity create a getter. The numeric index becomes a required parameter for the getter.

Declaring Arrays

Arrays can have a compile-time fixed size, or they can have a dynamic size.

To declare an array of fixed size, you specify the type of the elements and the number of elements required by an array as follows:

type arrayName [ arraySize ];

This is called a single-dimension array. The arraySize must be an integer constant greater than zero and type can be any valid Solidity data type.

For example, this is how you would declare a 10-element array called balance of type uint:

uint balance[10];

Note: Once you have declared a fixed size array then its size can never change.

What about dynamic arrays? With a dynamic array, the size of the array is not predefined when it is declared. As the elements are added the size of the array changes.

This is how you would declare an array of dynamic sizes in Solidity:

type [] arrayName;

Initializing Arrays

You can initialize array elements either one by one or using a single statement like this:

uint balance[3] = [1, 2, 3];

The values you assign to an array cannot be larger than the number of elements that we declare for the array.

Create an Array Contract

Make sure File Explorers is shown in the side panel. If not, click the icon in the icon panel to show it.

Click on the new file icon and create a new file titled ArrayDemo.sol.

For the first two lines in our contract, we will add our SPDX-License-Identifier and a pragma statement with our version of solidity. It should look like this:

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

Next, we will define our contract. Following the rule-of-thumb for naming contracts, it will be named the same as our filename like this:

contract ArrayDemo {

}

The goal of this contract is to get first-hand experience using the array state variable that is available in Solidity. Let's define three arrays -

  • integerArray - a fixed-length array of integers that has 5 items
  • boolArray - a fixed-length array that is assigned two values of true and false
  • addressArray - a dynamic sized array of addresses

Here is what it looks like:

contract ArrayDemo {
    uint[5] integerArray; // fixed size array
    bool[] boolArray = [true, false]; // assigned values
    address[] addressArray; // dynamic sized array or addresses
}

Our integerArray will contain five items. We did not set the values when we defined this state variable as we did with boolArray.

Previously we learned how the constructor can be used to set values when the contract is initially built. Let's take advantage of this by setting the integer Array to contain the values 1,2,3,4,5. Here is what the constructor should look like:

constructor() <span class="hljs-comment">{
    for (uint i = 0; i < 5; i++) {
        integerArray[i] = i + 1;
    }
}

Remember that arrays are zero-based. This is why we set the value to be equal to i + 1;

Now that we have set the values, we need to write a function that will return the value of any item in the array. Let's call the function getIntegerArrayItem. We will need to pass into the array an index. The function then returns the value in that position in the array.

Our function looks like this:

function getIntegerArrayItem(uint i) public view returns(uint) {
     return integerArray[i];
}

The last array we have is our dynamic array of addresses. We will need to write both a getter and a setter function for this array.

Let's start with writing the getter function that we will call getAddress. This function will require the user to pass in the index and we will return the address at that index.

This function is very similar to our getIntegerArrayItem function. Here is what the function looks like:

function getAddress(uint i) public view returns(address) {
     return addressArray[i];
}

Next, we need to write a setter function that will add a new address to the array. Users will need to pass in the address to the function. We will name the function addAddress.

Here is the function:

function addAddress(address _newAddress)  public {
    addressArray.push(_newAddress);
}

Challenge Section

Our getIntegerArrayItem function currently accepts any value for the index. Since our array has only 5 values we know the valid possible values for the index has to be 0, 1, 2, 3, or 4. Update our function to check the index the user passes in to make sure it is valid and if not always return the first item in the array.

Write a function to update the value of any item in our integerArray to a new value that is passed into the function.

Write a function that will return the number of addresses that are in our addressArray.

Structs in Solidity

A struct in solidity is just a custom type that you can define. The Solidity documentation defines them as "objects with no functionalities".

Structs in Solidity are a collection of variables (that can be different types) under a single name.

Defining a Struct

You define the struct with a name and associated properties inside it. This enables you to group many variables of multiple types into one user-defined type.

You define a struct in Solidity with the struct keyword, followed by the struct name and a pair of curly braces. The rule of thumb is the name of the struct is capitalized.

struct Book {

}

Within the curly braces, you must define the struct members, which consist of variable names along with their types.

struct Book {
    string name;
    string author;
    uint isbn;
}

Let's look at an example of how you can use structs:

Struct Example

You want to track all the books in a library. Each book will have a title, an author, and an ISBN number. To do that you will create an array with each item in the array being a book struct.

Create a Struct Smart Contract

Make sure File Explorers is shown in the side panel. If not, click the icon in the icon panel to show it.

Click on the new file icon and create a new file titled StructDemo.sol.

For the first two lines in our contract, we will add our SPDX-License-Identifier and a pragma statement with our version of solidity. It should look like this:

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

Next, we will define our contract. Following the rule-of-thumb for naming contracts, it will be named the same as our filename like this:

contract StructDemo {

}

We will define our book struct next in the contract:

contract StructDemo {
    struct Book {
        string name;
        string author;
        uint isbn;
    }
}

Now that we have our user-defined struct we need to define an array that will consist of nothing but our struct. We will call this array books.

Book[] books;

Next, we need a mechanism to add a new book to our library. Let's create a function called addBook that will do this:

function addBook(string memory _ name, string memory _ author, uint memory _ isbn) public {
    books.push(Book({ name: _ name, author: _ author, isbn: _ isbn }));
}

Now we need a way to return the details for a specific book in our array of books. Here is the method getBook where we pass in the index of the book.

function getBook(uint _ index) public view returns (Book memory book) {
    return books[_ index];
}

Challenge Section

Currently, our function to get a book assumes the user knows the index. But what if they only know the name of the book or the author? Create a function that will allow users to pass in either the title or author of a book and you return the correct book. If the book is not found then return a book where the value for the author, title and ISBN all say `not found.

Mapping in Solidity

So far we have covered the reference types of arrays and structs. The last reference type we will cover is mapping.

In Solidity, a mapping is referred to as a hash table, which consists of key type and value type pairs.

Declaring a Mapping

Here is the syntax to declare a mapping reference type:

mapping(_keyType => _valueType) public mappingName;

Where

  • _keyType - can be any value type. The key cannot be a reference type or a complex object
  • _valueType - can be any type

Consider the table below. If you ask for the value of key 103 you will receive Paul. hash table example

Mappings are incredibly useful for associations and frequently used to associate unique Ethereum addresses with associated value types.

Mapping can only have the type of storage and is generally used for state variables. If a mapping is marked public, Solidity automatically will create a getter function for it.

Mapping Example

Let's put what we just learned about mapping to use. We previously learned about structs by creating one that tracks books in a library. For this smart contract, we will create a struct that tracks the Instructors at a college.

The struct will capture each Instructor's first name, last name, and the subject they teach.

Create a Mapping Smart Contract

Make sure File Explorers is shown in the side panel. If not, click the icon in the icon panel to show it.

Click on the new file icon and create a new file titled MappingDemo.sol.

For the first two lines in our contract, we will add our SPDX-License-Identifier and a pragma statement with our version of solidity. It should look like this:

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

Next, we will define our contract. Following the rule-of-thumb for naming contracts, it will be named the same as our filename like this:

contract MappingDemo {

}

This map will save a number “value” to a corresponding address “key” and save the data to the blockchain. By using the address as the key, we will be able to look up the value for it.

mapping (address => uint) public myMap;

Next, we need a function that will allow us to add a value to any specified Ethereum address.

We will call the function addAmount. Here is what it looks like:

function addAmount(address _addr, uint _i) public {
    myMap[_addr] = _i;
}

Next, we need a function that will allow us to get the value assigned to an address. We will call this function getAmount. Here is what it looks like:

function getAmount(address _addr) public view returns (uint) {
    return myMap[_addr];
}

Using Structs as value types in mappings

In the previous example, I showed a simple mapping. What if your data structures are more complex? In that case, you can use a struct or even arrays in your mapping.

Let's create a struct to track Instructors. The struct will contain values for the instructor's age, first name, and last name.

Here is our struct:

struct Instructor {
    uint age;
    string firstName;
    string lastName;
}

Next, we need to declare a mapping. We will use an Ethereum address as the key in our mapping and the value will be our struct. Here is our mapping:

mapping(address => Instructor) instructors;

Let's populate our struct with a default value. We can use the constructor to do this. In order to populate our first value in our mapping, we need a valid Ethereum address.

If you have MetaMask or WalletConnect, you can copy the address from your wallet.

If not, remember in previous lessons, I talked about Remix providing you with 10 Ethereum accounts that you can use. Go to your Deploy & Run tab and copy the address of the first of the 10 Ethereum accounts assigned to you.

Now that we have an address then we can add a constructor to populate our first mapping entry. Here is what it looks like:

constructor() {
    instructors[0x5B38Da6a701c568545dCfcB03FcB875f56beddC4] = Instructor(
        {
            age: 17,
            firstName: 'Jennifer',
            lastName: 'Bland'
        }
    );
}

Feel free to change the values for first and last name and age to be your name and age.

Next let's write a function that will add a new entry to our mapping. We will follow the format we used in our addAmount function that we created for our simple mapping earlier.

Here is what the function looks like:

function addInstructor(address _instructorAddress, uint _age, string memory _firstName, string memory _lastName) public {
    instructors[_instructorAddress] = Instructor(
        {
            age: _age,
            firstName: _firstName,
            lastName: _lastName
        }
    );
}

Now that we can add entries to our mapping with our addInstructor function, we need a way to get values out of our mapping. We need a getter function. This will allow you to pass in an Ethereum address to the function and it will return the Instructors name and age.

Here is our getter function that I call getInstructor:

function getInstructor(address _instructorAddress) public view returns (uint, string memory, string memory) {
    return (
        instructors[_instructorAddress].age,
        instructors[_instructorAddress].firstName,
        instructors[_instructorAddress].lastName
    );
}

Structs and Mappings used with an Array

Let's do something a little bit more complex with a mapping. Previously I showed you how to use a struct as the value of a mapping. Now I am going to show you how to use arrays in conjunction with a mapping that contains a struct.

In our previous example of instructors, it is not possible to return all items in the mapping. For example, if you want to get a list of all the instructors and then iterate over them to return the instructors that meet a specific requirement, you cannot do this.

You cannot do it because by default a mapping contains all the possible values by default. There is no way to iterate over it.

This is where using an array with your mapping becomes a very powerful tool. You can create an array of all the items in your mapping.

This may sound confusing at first but let's walk through an example using our Instructors mapping.

We want to create an array to account for every instructor's address. Here is our definition:

address[] instructorAccounts;

Now when we create a new instructor we want to add this instructor's Ethereum address to our new array.

Go back to your constructor and we will need to add one line to add the address to our array. Your updated constructor should look like this:

constructor() {
instructors[0x5B38Da6a701c568545dCfcB03FcB875f56beddC4] = Instructor(
    {
        age: 17,
        firstName: 'Jennifer',
        lastName: 'Bland'
    }
);
instructorAccounts.push(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4);
}

We will also need to add this to our addInstructor function. Instead of using the same line we used in the constructor, we will change it so that we use the address that was passed into the function. Your updated addInstructor function should look like this:

function addInstructor(address _instructorAddress, uint _age, string memory _firstName, string memory _lastName) public {
    instructors[_instructorAddress] = Instructor(
        {
            age: _age,
            firstName: _firstName,
            lastName: _lastName
        }
    );
    instructorAccounts.push(_instructorAddress);
}

We can now create a new function that will return the number of instructors in our mapping. We can do this easily because we have an array of addresses and the length of that array will tell us how many instructors we have.

Here is our function that we will call getInstructorCount:

function getInstructorCount() view public returns(uint) {
    return instructorAccounts.length;
}

Earlier I mentioned that we could not iterate over a mapping. We can with an array. Let's combine our mapping and our array to write a function that would return the total count of instructors that are over a specified age. The specified age will be passed into the function as a parameter. We will call our function getCount.

We will loop through all the entries in our array. Each entry is the Ethereum address of an instructor. We can use that address to get the Instructor struct from our mapping. We will then check if the instructor's age is greater than the specified age. If it is, we will increment a counter. Here is what our function looks like:

function getCount(uint _age) public view returns(uint) {
    uint total = 0;
    for (uint i = 0; i < instructorAccounts.length; i++) {
        if (instructors[instructorAccounts[i]].age >= _age) {
            total++;
        }
    }
    return total;
}

Challenge Section

For our simple mapping called myMap, write a function that will delete an address and its value.

Functions in Solidity

A function is a group of statements that perform a specific task. Functions allow a programmer to break down a contract into smaller pieces.

Function Definition

The most common way to define a function is to use the keyword function, followed by a unique function name, a list of parameters, and a block of code surrounded by curly braces.

functionName(parameter1, parameter2, parameter3) visibility mutability {
    // code goes here
}

If a function returns a value, it must be specified in the return statement. The value of the item to be returned must be defined in the function definition.

functionName(parameter1, parameter2, parameter3) visibility mutability returns (returnType) {
    // code goes here
    return returnValue;
}

Examples a Function

The first example above shows a function that does not return a value. These functions are referred to as setter functions because they set the value of a state variable. Here is an example of a setter function:

function setValue(uint _value) public {
    value = _value;
}

Sometimes a function will return a value. These functions are referred to as getter functions because they get a value. Here is an example of a function that gets a state value and returns it.

function getValue() public returns (uint) {
    return value;
}

Function Parameters

You can define parameters for a function by listing them in the parentheses after the function name. Here is an example of a function that adds two numbers together:

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

Calling a Function

When you want to execute a function, you must call it. You can call a function by using the function name followed by parentheses. If the function has parameters, you must pass them in the parentheses separated by commas.

If I want to call the getValue function, I would write:

getValue()

If I want to call the add function and pass in two numbers, I would write:

add(3, 4)

The passed parameters can be used inside the function body. A function can have multiple parameters separated by commas.

Visibility

There are four visibility levels for functions:

Functions can be specified as being external, public, internal or private. The default is public.

public - public functions can be called from anywhere. private - private functions can only be called from within the smart contract where it is defined. They cannot be called externally. internal - internal functions can be called from within the smart contract where it is defined an d all smart contracts that inherit from it. external - can only be called from outside the smart contract.

State Mutability

There are three mutability levels for functions:

View - functions that do not modify the state. They can only read the state. pure - function that can neither read nor modify the state. payable - functions that can accept Ether sent to the contract.

Functions defined with view and pure keywords do not change the state of the Ethereum blockchain, which means when you call these functions you won't be sending any transaction to the blockchain. What happens instead is that the node you are connected to executes the code of the function locally by inspecting its own version of the blockchain and gives the result back without broadcasting any transaction to the Ethereum network.

Fallback Function

A contract can have at most one fallback function. This function cannot have arguments, cannot return anything, and must have external visibility. It is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function.

Function Modifier

Modifiers are used when you want to check some condition prior to the execution of a function. We can also use modifiers to check if a function has been called with the correct parameters. For example, if you want to check if the sender is the owner of the contract you can write something like:

function selectWinner() external {
    require(msg.sender == owner, "this function is restricted to the owner");
    // code goes here
}
With modifiers, you can isolate the code that you want to execute from the code that you want to check the condition. You can declare a modifier as follows:

modifier onlyOwner() { require(msg.sender == owner, "this function is restricted to the owner"); _; // will be replaced by the code of the function }


And we add the modifier name to the function definition:

function selectWInner() external onlyOwner { // code goes here } ```

Multiple modifiers can be used on a single function by specifiying them in a whitespaced-separated list. Modifiers are evaluated in the order presented.

Require keyword

The require keyword is used to check that a function has been called with the correct parameters. This allows you to guarantee the validity of conditions that cannot be detected before execution. The require keyword will only allow execution of a function if a particular condition is met.

Did you find this article valuable?

Support Jennifer Bland by becoming a sponsor. Any amount is appreciated!