The Basic Solidity Guide

Introduction

As a web developer venturing into learning new languages is only a necessity, and one of the key features that make up a developer. deeming the rise of the new iteration of the internet (web 3), solidity has proven to be one of the primary and essential tools for developers transitioning into the space of web3. In this tutorial You'll learn all the fundamentals to get you started with understanding the basics of solidity, You'll be able to read smart contracts and make reasonable analyses of what's going on 🤔?

Solidity and Smart Contract

When transitioning into a new language, it is only logical to assume that you must have made all necessary research as to what and how a language scales in the real world, and also the kind of problem they solve. This tutorial is not aimed to make you an expert, but rather to give you a clearer picture of the basics of Solidity and get you started with the real-world practice... A quick recap

What is Solidity

Solidity is an object-oriented programming language used to build smart contracts that run on the Ethereum blockchain.

For a better recommendation for mastery, check out the Solidity Documentation that stands as a sure guide over your course of learning.

What is a Smart Contracts

The smart contract is a set of computational instructions executing transaction protocols that are deployed on a decentralized blockchain like (Ethereum)... they are immutable or incredibly hard to change once deployed due to consensus and they are automatically executed

Getting Started

Unlike other languages, you can always specify the version of solidity you want to use with a line of code, which you'll see often beginning with every smart contract

pragma solidity <version number>;

Where the version number is the specific version of solidity you want to compile your code.

Note: If you don't want to use a specific version, you can specify a minimum version number by adding the caret (^).

For example, the code below means the smart contract can compile with the solidity version 0.8.0 and above,

pragma solidity ^0.8.0;

but without the caret (^) sign, it uses the 0.8.0 specifically.

pragma solidity 0.8.0;

Initializing the smart contract

Every smart contract begins by defining the contract with the keyword contract or library followed by the name of the contract which can be anything.

contract  <ContractName> {
}

For Example:

contract  FirstContract {
}

Any code written in the brackets above will be the context of the contract FirstContract.

Libraries library: are similar to contracts, but only you cannot declare any state or send ether transactions. Libraries are embedded into the contracts, only if all the library functions are internal. They help to add more functionalities to different values. They are created just like contracts but with the keyword library.

library  <ContractName> {
}

For Example:

library  AnotherContract {
}

Types and Variable Declaration

Just like every other programming language, Solidity also supports primitive data types as such: uint int address booleans bytes, just like other languages, they are used to define variables. The format of which variables are declared in solidity is

<type> <variable name> = <value>;

when you initialize a variable without adding a value the, its value is automatically assigned as 0;

Unsigned integeruint: Similarly to the unsigned data types in SQL they carry only positive numbers.

uint linesOfCodeWritten = <positive numbers>

this means that the variable linesOfCodeWritten can only be assigned a positive value. For example:

uint256 linesOfCodeWritten = 150

when using the uint type, you can also declare a specific byte range if you're certain about its value size, an example: uint8 for 8 bytes or uint32 for 32 bytes, but without specification, it'll automatically be declared as uint256.

Integers int: They are the opposite of uint because they carry signed integers which include positive and negative numbers, but similarly to the uint they are be prespecified in size, int16 for 16 bytes or int64 for 64 bytes

int codeLineAdded  = 10
// unsigned integer

and will also be automatically declared as int256 when size is not specified.

int codeLineRemoved  = -10
// signed integer

Boolean bool: They simple define the type true or false.

bool  hasWrittenCode = <true> or <false>

which means the variable hasWrittenCode is defined as a boolean that can either be assigned the value true or false.

Address address: These are special 20byte types in the format: 0xAb098b54332Ae1Fd23Bf0e7ac83aADD123456789 used to interact with the blockchain, and are declared with the keyword address.

Strings string: They are used to define words or phrases, using the string keyword and adding quotes around its value.

string yearsOfWorking = "fifteen";

Bytes bytes: These are special types, they are similar to strings since they are also used to store text types and are used the same way, but they also allow you to prespecify their size. byte4 for 4 bytes or byte32 for 32 bytes string.

bytes32 name = "Phensic"

their maximum and default byte size is byte32

Comments are any kind of primitive type but cannot be executed or compiled as codes. Single line comments are written by adding // at the beginning of the line, while multiline comments are written as{/* code here */}.

// single line comment 
{/* 
code line here 
code line here 
code line here 
*/}

Basic Functions

Functions in Solidity, like every other language are self contained modules that execute a subnet of code or instructions when they are called. Using the keyword function in the format:

function <function name>(<variable1>, <variable2>) public returns (<output type>) {
<subnet of code instructions>
}

To create a function using the format above, that modify the uint variable linesOfCodeWritten after adding and removing a few lines of code.

function codeEddited (uint256 _codeAddded, int _codeRemoved) public {
uint newLineOfCode = linesOfCodeWritten  + _codeAdded - _codeRemoved 
}

the returns keyword in the format is not compulsory when creating a function, but is used to predefine the type of output expected to be returned from the function.

Visibility Specifier

Unlike the returns keyword, the visibility specifiers like public are compulsorily added and are used to determine where a function can be called and accessible.

Public public: means the function can be accessed outside the current contract. When used, it creates a getter function or a state variable.

function yourName public () {
string name = "Phensic"
}

the public function above can be called outside the contract and will return the value of the variable name denied in the function.

Private private: When used? they can only be accessible inside the contract, and they can be used to declare a variable or function.

External external: They are only visible and accessible externally and can only be used for function.

Internally internal: They are only visible and accessible internally.

Note: the underscore (_) is usually added as a naming convention for creating function variables.

Mapping Function

Mapping is a data structure that has keys mapped to a single variable. They are special types of functions similar to dictionaries because can they assign variables to values by looping through an array. using the keyword mapping in the format:

mapping (<from type> => <to type>) public <function name>{
}

The mapping function nameToAge:

mapping (string => uint256) public nameToAge{
}

created can be used to map a set of string variable types to a set of uint256 variable types.

And to use the function, you can call the nameToAge mapping function like this:

function addPerson(string memory _name, uint256 _age) public{ 
      people.push(People(_age, _name));
      nameToAge[_name] = _age;
}

the new public function addPerson uses the mappping function nameToAge to loop through the people array and map the variable _name to the variable _age.

Note: Functions created using the keyword view or pure, simply means they disallow you to modify the state of the function, but you can only read their states from the blockchain.

Arrays & Structs

Structs in Solidity are used for creating new types by adding the keyword struct before the name of the type you want to create.

struct <type name> {
<variable1> ;
<variable2>;
}
struct People {
uint256 age;
string name;
}

the struct keyword Is used to create a new type People that has two variables names, age and name in the code above.

People public person({
age: 20
name: Phensic
});

the code above creates a new variable person with the type People:

Arrays takes the use of a struct farther by using it to create a list of objects in the same format used to create the person variable but adding a square bracket after the type name [] to declare the array;

People[] public allpeople

To reset an array of objects.

people = new People[] {0};

Dynamic and Fixed arrays:

Dynamic arrays are declared by default without adding an integer in the bracket, which means you can add as many lists as you want.

Fixed arrays are declared by adding an integer in the bracket, which means you cannot exceed a specific number of a list-objects.

Writing a function that adds to an existing array of type Peoplewould look like:

function newPerson(string memory _name, uint256 _age) public {  
people.push(People(_age, _name)); 
}

Memory Calldata and Storage

If you've noticed the keyword memory used in a few functions earlier, they are keywords used to declare where and how a variable should be stored in solidity. There are 6 storage specifiers, stack, memory, storage, calldata, code log.

Memory memory: is used to declare temporary variables that can be modified, which means they will only exist during the time of the transaction. For example:

function newPerson(string memory _name) public {  
string yourName = _name
}

the variable _name declared as a string with the memory keyword means that it will only exist in storage at the time of a transaction.

Calldata calldata: are used to declare temporary variables that cannot be modified, they are like constants but are also temporary since they only exist at the time of the transaction.

string name = "Julius"
function newPerson(string calldata _name = "Phensic") public {  
_name = "Julius"
}

the code above will give an error because you cannot modify a variable stored in thecalldata.

Storage storage: all variables declared by default are stored in the storage, they are permanent (can exist even after the transaction time), and can be modified.

Note: you cannot define variable storage with the stack, code, or log.

Solidity Loops

For Loops lopping in solidity is quite similar to other languages. Looping through an array or a list in solidity uses the for keyword in the following the format:

for (<starting index>; <ending index>; <step amount>) {
// code to execute on the index
}
uint256 finalIndex = 10
for (uint256 firstIndex = 0; firstIndex < finalIndex; firstIndex++) {
// code to execute on the index
}

The for loop above starts a loop from the value of firstIndex (0) and runs any code in the execution space {} as long as the firstIndex (0) is less than the value of finalIndex (10), and increases the value of firstIndex, till it is no longer less than the finalIndex (10).

Require require: the require keyword in solidity work similarly to if-else stataments in other languages, using the format:

bool isOldEnough = true;
require(<if statment>, <else-statement>)

the require keyword creates an if-else-like scenario, where if the first expression before the comma does not hold true, the statement after the comma is returned.

bool isOldEnough = true;
require(isOldEnough, "Sorry! you're not old enough.");

In the require statement above since the variable isOldEnough equals true, the statement after the comma will not be returned.

In other words the sentence "Sorry! you're not old enough." will only be returned if the expression before the comma holds false.

Composability and Smart Contracts Interactions

Composability When working with a large-scale project that requires using more than one contract or ledger for transactions. In solidity, contracts can interact with other contracts, the same way you can import modules in javascript or python, etc. the process is called Composability.

You can either decide to write a multi-contract file, with more than one contract on one page.

pragma solidity ^0.8.0;

ledger SimpleCurrencyConversion{
// contract about currency conversion
}

contract TransactionHistory {
// contract about transaction history
}

contract usersDetails {
TransactionHistory public transactions;

function createNewTransactionHistory() public {
        transactions = new TranactionHistory();
}

In the contract file above, the userDetails contact is deploying a new TransactionHistory contract.

Here, the 3 contracts above can be composed together by calling functions from one contract into the other.

This means that a simple solidity file can hold more than one contract.

Another more flexible way to interact with other contracts, without having too many lines of code, which is not good practice is using the import keyword. Similarly to other languages, you can import other contracts from different files using the format:

import <solidity file path>

From the multi-contract file earlier, if each contract and ledger had its own files you can import them like this:

pragma solidity ^0.8.0;

import "SimpleCorrencyConversion.sol";
import "TransactionHistory.sol";
contract usersDetails {
}

if the current contract file was the userDetails file, you would be able to import the other contract file like in the code above.

Smart Contract Interaction In order to be able to make interactions with other contracts, like calling functions and variables defined in other contracts files. To make this interaction, you'll need to get the contracts -Application Binary Interface- ABI and the contract's address, where the contract is deployed to.

Note: Although they are usually added automatically after importing the outer function above the bae contract. The ABI of the contracts defines how the base contract can interact with the other outer contracts, they contain information like the outer contract's byte code, the functions, and the variable declared in the contract.

inheritance & Overrides

When interacting with functions and variables in other contracts, solidity also supports inheritance, which means you can modify the use of a function from a child contract created in the parent contract.

Parent Contract:

pragma solidity ^0.8.0;


contract TransactionHistory{
uint256 usersAge = 20

function store (uint _addedAge) public virtual{
    usersAge  =  _addedAge; 
    } 
}

In the parent contract, the store function simply changes the value of the variable userAge.

The virtual keyword tells solidity that the store function can be modified in any child contract

Child Contract:

pragma solidity ^0.8.0;
import "TransactionHistory.sol"

contract UserDetails is TransactionHistory{
function store (uint256 _yearsPast) public override {
     usersAge  =  usersAge + _yearsPast; 
    }
}

The is enables the current UserDetails contract to inherit all the functionalities of Transaction contract.

The override enables the function to rewrite the use of the store function from the parent contract.

Sending Funds In Smart Contract

When making transactions that involve sending tokens, they require creating a function with the keyword payable in the format:

function <function name> public payable {
}

When using a payable function, you get access to certain global keywords that handles payments msg. For example, you can set a value to send as msg.value. To get the address of the sender: msg.sender. To transfer funds in contracts:

payable(msg.sender).transfer(<address>(this).<amount>);

the transaction above will semple be reverted if it does not go through and there was an error. Other ways of smart funding contracts are: Using the send keyword, which will only return a boolean of true or false if the transaction was success or not.

bool sendSuccess = payable(msg.sender).send(<address>(this).<amount>);
require(sendSuccess, "Send failed");

And the call keyword is a rather lower-level command or sending fund, and can also be used to call any function in Ethereum. Using the call keyword in a format similar to the format.

(<boolean variable>, <byte variable>) = payable(msg.sender).call{value: <address>(this).<amount>}("");
(bool isSuccess, byte details) = payable(msg.sender).call{value: <address>(this).balance}("");

the two variable declared at the beginning of the function is returned by default when using the call keyword. Therefore the bool variable is declared the store the returned boolean of if successful and if not. And the byte variable also stores the information about the transaction.

Check out other features of payable functions, how to make a payable transactions with the .msg keyword, and other transactions on contracts.

Interfaces & package management

Interfaces are basically standardized deployed contracts. In simple terms, interfaces are standardized smart contracts that can be accessed using the npm package manager. They are means of interacting with already written contracts outside your projects. To use an interface, all you need to do is import the contract directly from its location in the npm package manager or on Github. They usually start with the @ sign. An example of how interfaces are imported into a smart contract project:

import "@chainlink/contracts/src...sol";

more on Interface.

Constructors

Constructors constructor: These are similar to the constructor functions in Javascript, using the constructor keyword, they are called immediately after deploying a smart contract. The simple format.

    constructor () {
    uint256 minimumAmount = 10;
    }

Modifiers

Modifiers modifier: Are used for functions that can be reused in other functions, they help to reduce repeatability. the format for creating and using the modifier.

modifier <name>{<code here>}

function transaction() public onlyOwner {
uint256 amountLimit = 100;
}


modifier onlyOwner {
  string owner = "Phensic";
  _;
}

The onlyOwner is a modifier function that assigns a value "Phensic" to a variable owner, and can be reused in any other function that has onlyOwner in its function declaration. The underscore means (run any other code after the code in the modifier function), which means the value Phensic gets assigned first every time the modifier onlyPower is used before any other code.

Immutables And Constants The constant keywords are used to create constant variables, variables you don't intend to change throughout the contact. Using the constant keyword in the format:

uint256 public constant AMOUNT_LIMIT: 1000;

Note: It's always good practice to use constants where needed, and to use the uppercase for calling their variable name for clear readability.

While immutable is used to create constants that are only assigned once after being initialized.Using the immutable keyword in the format:

address public immutable i_owner;

constructor () {
    i_owner = msg.sender;
}

Since the i_owner variable is only assigned once after being initialized, you can declare it as an immutable available, which also means it cannot be changed.

Note: Global and Local variables act exactly the same in solidity as in other programming languages. Global variables are created directly Inside the contract and are accessible anywhere inside the contract. Local variables are created onside functions and cannot be accessed outside their declared functions.

Conclusion

Congratulations on completing the solidity tutorial guide.

Whether you are a first-time developer learning about contracts or an experienced developer transitioning into the web3 space, you have a basic grasp of how solidity is used or writing smart contracts.

If you've completed this guide, you've taken a big step into transitioning into the web3 space. Now you'll be able to understand the basic functionalities that make up a smart contract.

Congratulations again.

Did you find this article valuable?

Support Mayowa Ogungbola by becoming a sponsor. Any amount is appreciated!