Table of contents
- Introduction
- Solidity and Smart Contract
- What is Solidity
- What is a Smart Contracts
- Getting Started
- Initializing the smart contract
- Types and Variable Declaration
- Basic Functions
- Visibility Specifier
- Mapping Function
- Arrays & Structs
- Dynamic and Fixed arrays:
- Memory Calldata and Storage
- Solidity Loops
- Composability and Smart Contracts Interactions
- inheritance & Overrides
- Sending Funds In Smart Contract
- Interfaces & package management
- Constructors
- Modifiers
- Conclusion
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 People
would 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.