- Variables
- Contracts
- Constructors
- Functions
- For Loops and Conditional Statements
- Guard Functions
- Libraries
- Type Casting
- Funding and Withdrawing Ether
- Blockchain Specific Variables
- Conclusion
Solidity is an object oriented programming that shares many of its syntax with other statically typed languages like Java, and does the process of verifying and enforcing the constraints at compile-time as opposed to run-time like Python.
This language is used for implementing smart contracts that are deployed on blockchains platforms like Ethereum. Therefore, Solidity has a very specific purpose that is focused on consistency and security.
A Smart Contract? What is that? Smart contracts are simply applications stored on a blockchain that run when particular conditions are met. The idea is that smart contracts automate a workflow in which participants can be immediately certain of the outcome.
But wait, What is a blockchain? Basically it’s like a database where the information is stored in a way that makes it difficult or almost impossible to change, hack, or cheat the system.
Some of the main features of a blockchain are:
- Immutability. The blockchain cannot be altered.
- Distributed. A copy of the blockchain is shared by many participants.
- Decentralized. There is no government or organization that is responsible for deciding what happens in the blockchain.
- Secured. The records in a blockchain are encrypted.
Variables
There are three types of variables in Solidity:
- State variables are those variables in Solidity that are permanently stored in a contract storage.
- Local variables are those present while a function is executing.
- Global variables are those that exist in the global namespace used to get information about the blockchain.
The types in Solidity are:
- Booleans:
bool
- Integers:
int
oruint
, signed and unsigned respectively. You can specify how many bits you want to use withuint8
touint256
. Integers get initialized to zero. - Address:
address
. An address represents a 20 byte value, which is the size of an Ethereum address. Address can have thepayable
modifier so it includes two additional operationstransfer
andsend
. - Byte Array:
bytes
- Arrays:
uint256[]
:- Declaration: a single-dimension array
uint256 myArray[10] = [1, 2, 3]
and a dynamic arrayuint256 myArray[] [1, 2, 3]
. - Add element:
myArray.push(1)
where1
is a value. - Get element:
myArray[0]
where0
is the index. - Get length:
myArray.length
. - Reset array:
myArray = new uint256[](0)
- Declaration: a single-dimension array
- Mapping:
mapping
types define a key value structure. e.g.mapping(uint => address) map
- Enum: This is to enumerate a variable to have only some predefined values. e.g.
enum ROLES{ADMIN, USER, ENGINEER}
- Struct: Structs are used to group a number of variables together. e.g.
struct struct_type { }
Access modifiers:
public
: anyone can see what is stored in the variable. Global scope.private
: only visible inside the contract.internal
: only visible inside the contract and children contracts. The default modifier isinternal
when no modifier is specified.
You can declare a variable as constant
and this means the variable cannot be assigned again. It saves gas because the value is stored directly into the bytecode of the contract. The convention is to use capital letters and underscores e.g. MINIMUM_USD
.
An alternative to constant
is immutable
. In this case the variable can only be assigned once at construction time. It saves gas. The convention is to prefix the variable with i_
e.g. i_owner
.
Contracts
A contract is like a class in Java and has the following structure:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;
contract MyContract {
// content of the contract
}
As you can see we are including on top of the contract two things:
- A license identifier:
SPDX-License-Identifier
. It is used to indicate the license we are going to use for the contract, which isMIT
. - The solidty version for the contract:
pragma solidity ^0.8.8;
In this case we are saying we can use any version from0.8.8
for compiling the contract. You could also specify a range likepragma solidity >=0.8.8 <=0.8.10;
Use
//
or/* */
for commenting.
You can use the public
modifier to make variables inside the contract public.
Constructors
Similar to Java you can also declare a constructor in Solidity.
- A constructor runs only once in the entire lifetime of the contract when it’s created.
- It’s used to initialize contract state.
- A contract can have only one constructor.
- In case, no constructor is defined, a default constructor is present in the contract.
contract MyContract {
address public immutable i_owner;
constructor() {
i_owner = msg.sender;
}
}
Functions
Functions are declared like this:
function myFunction() returns (uint256) {
return 1 + 1;
}
Function Built-in Access Modifiers
public
: makes the function visible inside and outside the contract:
function myFunction() public {
// content of the function
}
external
: makes the function visible inside and outside the contract:
function myFunction() external {
// content of the function
}
What is the difference between
public
andexternal
? The difference is that in public functions, Solidity immediately copies the function argument to memory, while external functions can read directly fromcalldata
. Therefore, there is a gas cost implication. Memory allocation is expensive, whereas reading fromcalldata
is cheap. As for best practices, you should use external if you expect that the function will only ever be called externally, and use public if you need to call the function internally.
internal
: This is to make the function protected, so only the contract where the function is defined and its children contracts can see the function:
function myFunction() internal {
// content of the function
}
private
: The function can only be called by other functions in the same smart contract.
Pure/View/Payable Functions
view
: to make the function read-only, the function can access the state but can’t make any modification:
contract MyContract {
uint x = 1;
function myFunction(uint y) pure returns (uint) {
return x + y; // it's reading x from outside the function
}
}
pure
: ensures that the function cannot read or modify the state:
contract MyContract {
uint x = 1;
function myFunction(uint i, uint j) pure returns (uint) {
return i + j; // it's not reading any variable from outside the function
}
}
payable
: payable functions can change the state. Fon any function that will induce a transfer of assets, you must use thepayable
type to the function and address.
receive() external payable {
}
Use an underscore (
_
) in front of a function attribute to differentiate it from the ones de declared on the contract scope:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;
contract MyContract {
uint myNumber; // this get assigned to zero
function myFunction(uint256 _myNumber) returns (uint256) {
myNumber = _myNumber;
}
}
Custom Function Modifiers
You can define custom function modifiers within a contract like this:
modifier onlyOwner {
require(msg.sender == i_owner, "Sender is not owner!");
_; // call the rest of the code in the function
}
As you can see above we first execute some code and then we use _
. Underscore is to tell Solidity to execute the rest of the code in the function. You could invert the order and execute first the code in the function and then the code in the modifier.
Functions Data Location
Variables can be declared as storage
, memory
or calldata
to explicitly specify the location of the data.
memory
and calldata
are used for temporarily store variables and their values. You can only specify memory
or calldata
for array, struct, mapping or string types.
What is the difference between
memory
andcalldata
?calldata
is only valid for arguments ofexternal
functions and behaves mostly likememory
. Any variable defined ascalldata
cannot be modifiable, whereas a variable defined asmemory
can be modified within the function. As a resultcalldata
variables definition incur in less gas fees thanmemory
variables.
function myFunction(uint256 _myNumber) returns (uint256) {
myNumber = _myNumber;
}
storage
is used to defined variables that we want to write on the blockchain. These variables are persistent since they are written to the blockchain. storage
variables can be accessed from anywhere inside and outside the contract. Global variables are storage
variables by default. Variables defined as storage
always will incur gas fee.
Function Returning Values
Functions can return one or multiple values. If a function returns multiple values you can assign those values to variables like this:
(uint80 roundID, int price, uint startedAt, uint timeStamp, uint80 answeredInRound) = priceFeed.latestRoundData();
You can also omit those values that you don’t need:
(, int price, , ,) = priceFeed.latestRoundData();
For Loops and Conditional Statements
The for
loop syntax is like the Java one:
for(uint256 funderIndex = 0; funderIndex < funders.length; funderIndex++) {
address funder = funders[funderIndex];
}
The if
, else if
and else
statements are also inherited from languages like C or Java:
uint a = 1;
uint b = 2;
uint c = 3;
uint result
if( a > b && a > c) {
result = a;
} else if( b > a && b > c ){
result = b;
} else {
result = c;
}
Guard Functions
assert()
and require()
functions are guard functions that are used to improve readability.
You can use require()
for checking a condition and in case the condition is not met throw an error with a message and revert the transaction. The first argument is the condition and the second argument is optional and the message.
require(msg.value.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
assert()
works similarly to require()
, but it’s used for internal errors.
The main difference between assert()
and require()
is that the former uses up all the remaining gas and reverts all the changes made whereas the later also reverts back all the changes made to the contract but does refund all the remaining gas fees.
An improvement over assert()
and require()
would be to use revert()
. You can declare an error and then call revert()
with the error when a particular condition is not met. This is more gas efficient because strings use storage.
error NotOwner();
contract MyContract {
if(msg.sender != i_owner) {
revert NotOwner();
}
}
Libraries
Libraries look similar to contracts but they are not contracts and the purpose is different. A library contains a set of reusable functions that can be called in your contract, so you keep your code organized, reusable and easy to understand.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
library MyLibrary {
function getSum(uint256 value1, uint256 value2) external view returns (uint256) {
return value1 + value2;
}
}
To use a library within a smart contract, we use the syntax using MyLibrary
for Type. We could use *
instead of a specific type to attach functions from the library to all types.
import "./MyLibrary.sol";
contract MyContract {
using MyLibrary for uint256;
uint256 a = 3;
uint256 b= 5;
function printSum() public {
print(a.getConversionRate(b));
}
}
As you can see above, when you call a library function, the function receives the object they are called on as its first parameter, then you can pass the rest of the parameters are function arguments.
Type Casting
Solidity is a statically typed language, but sometimes you might need to convert one type to another explicitly.
value = uint256(msg.value);
Funding and Withdrawing Ether
Funding
You can fund your contracts by declaring functions with the access modifier payable
. If you try to send ether to a function without a payable
modifier, the transaction will fail.
Call the following function some Ether to fund the contract where the function is defined.
function deposit() payable {
}
Withdrawing
There are three ways of sending funds to a particular address:
transfer()
. There is a gas limit of 2300 and would throw an error if the transfer fails.
payable(msg.sender).transfer(address(this).balance);
send()
. There is a gas limit of 2300 and returns the status as a boolean.
bool sendSuccess = payable(msg.sender).send(address(this).balance);
require(sendSuccess, "Send failed"); // revert if it fails
call()
. This is the recommended way of transferring Ether and there is not gas limit. It returns the status as a boolean.
(bool callSuccess, bytes memory data) = payable(msg.sender).call{ gas: 10000, value: address(this).balance }("");
require(callSuccess, "Call failed"); // revert if it fails
Blockchain Specific Variables
You can fetch information from the blockchain itself and use that date in your functions. Two variables are used : msg
and block
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
contract MyContract {
// other functions
function getGasLimit() public view returns (uint256) {
return block.gaslimit;
}
function getSender() public view returns (address) {
return msg.sender;
}
}
Conclusion
Learning about blockchain and Solidity are skills that can really widen your horizon, not just technically but career wise as well. There’s still a lot of industries where blockchain hasn’t reached its true potential and learning Solidity will give you an upper hand in the coming years.