
What is Solidity?
Solidity is a high-level language for implementing smart contract in Ethereum environment. Solidity is statically typed, supports inheritance, libraries and complex user-defined types among other features.
The contract you create with Solidity is immutable once you deployed it. So most of the times you need to run many tests with your contract to ensure that it is run without bugs.
From the docs: Contracts in Solidity are similar to classes in JavaScript (I know... it's not a real class). They contain persistent data in state variables, and functions that can modify these variables.
Calling a function on a different contract (instance) will perform an EVM function call and thus switch the context such that state variables in the calling contract are inaccessible. A contract and its functions need to be called for anything to happen. There is no “cron” concept in Ethereum to call a function at a particular event automatically.
Why?
The main reason I choose this path is simple. Writing code in blockchain is like writing art. You need to really carefully craft your code because:
- You care about your user.
- You care about your code.
What that means is when you code in solidity and deploy it, it is immutable. You will not be able to patch it or add more feature to it. The only way to update it is to create new contract. So your code must be bug-resistant , going through a lot of tests , and fully optimize so your user can save gas cost.
Flash Tutorial
In this post we don't deep dive into Solidity. If you want to know more about it, you can always check the full documentation in their website . We are going to get to know only the basic of it so you can get a bird view vision of what it is and how to write it.
Default
- Functions is public by default.
- Comment is similar to JavaScript: `// comment`.
- Use camelCase.
- Use semi-colon (yeah I know...).
Pragma
Version pragma - declaration of the version of the Solidity compiler this code should use.
// SPDX-License-Identifier: MIT
pragma solidity >=0.5.0 <0.9.0;
Variables and Data Types
State variables are permanently store in contract storage . you can also save it in memory and it will get erased between functions called. If you put it on the storage it means they're written to the Ethereum blockchain. Think of them like writing to a DB. Other store is calldata which is same like memory but only for external functions.
List of Data Types:
- string : similar to JS, you can use either single or double quotes.
- uint : unsigned integer. Default to uint256.
- int : signed integer, Default to int256.
- address : special data type refer to the address of the account (20 byte value).
- address payable : same as address but with additional members `transfer` and ` send`.
- mappings : key value pair: `mapping (uint => string) greet`, uint is the key type and string is the value type.
- arrays : same like JS, use [], but it can be fixed or dynamic.
- fixed array: ` uint[2] numArr `
- dynamic array: ` uint[] dynamicNumArr `
- bool : boolean value: true, false.
- structs : complex data type (I think this one like Go (golang) struct ).
Struct example
struct Person {
string name;
uint age;
}
// create variable based on Person:
Person satoshi = Person("satoshi", 42);
Person[] people;
people.push(satoshi);
Reference data type: arrays, structs, mappings.
Typecasting
You can also do typecasting similar to JS.
uint8 a = 5;
uint b = 6;
uint8 c = a * uint8(b);
// address to address payable
payable(myAddress);
In the example above we typecast `b` from uint to uint8, so we can do the calculation safely and store it in uint8 c, otherwise it will throw an error.
Global Variables
Solidity have global variables . This is some of it:
- msg.sender : sender of the message (current call).
- msg.value : number of wei sent with the message.
- block.gaslimit : current block gas limit.
- block.number : current block number.
- block.timestamp : current block timestamp.
- require : abort execution and revert state changes if condition is false.
- keccak256 : compute the Keccak-256 hash of the input.
Operators
Operator in Solidity is similar to JavaScript, but with few exceptions like Solidity don't have `===`, which is Nice!
Conditional
Writing if-else in Solidity is similar to JS.
if (x == 42) {
old();
}
Loops
Solidity for loops syntax is similar to JS, but you need to use uint instead of let or var for the iteration number.
uint[] memory evenNumber = new uint[](5);
for (uint i = 0; i < 10; i++) {
if (i % 2 == 0) {
evenNumber.push(i);
}
}
// evenNumber will be [0, 2, 4, 6, 8]
Functions
The functions syntax is similar to TypeScript (you need to declare parameters type and its return value type) but with different syntax.
function multiply(uint x, uint y) pure view returns(uint) {
return x * y;
}
Solidity functions are public by default. Which means everyone or every contract can access it. So it is best practice to specify it as private .
Function parameter by convention start with _, like ` uint _data` .
List of functions visibility specifiers and modifiers:
- public : default attributes.
- private : to make a function private, by convention add prefix _ to the function name. Private functions can't be inherit.
- internal : same as private except it can be inherit.
- external : same as public except it can ONLY be called outside the contract. Not by other functions in the contract.
- view : function don't alter any values or write anything.
- pure : function not accessing any data in the app.
- returns (): function have a return value.
Hash Function
Solidity built-in hash function: keccak256, a version of SHA3. Hash function basically maps an input into a random 256-bit hexadecimal number. Keccak256 expect a single parameter of type bytes .
keccak256(abi.encodePacked("abe"));
The abi.encodePacked is also consider as global variables.
Returns Multiple Values
Solidity allow you to do multiple return value. Not like JavaScript where you must return an array or object to do that.
Example:
function multipleGreet() returns(string memory en, id, sv) {
return ("hi", "hai", "hej");
}
function getMultipleGreet() {
string memory english;
string memory indonesian;
string memory swedish;
// get those multiple return value
(english, indonesian, swedish) = multipleGreet();
// or only get the indonesian greet
(, indonesian, ) = multipleGreet();
}
So it is like using array or object destructuring but with different syntax.
Events
Events are way for your contract to communicate that something happen on the blockchain to your app front-end, which can be 'listening' for certain events and take action when they happen.
event IntegersAdded(uint x, uint y, uint result);
function greet() public {
// lot of codes...
emit IntegersAdded(8, 9, 17);
}
Require
Require is a function that will throw an error and stop executing when some conditions is not true.
function isOld(uint _x) public {
require(_x == 42);
// if x is 42 the code below get executed
}
// So require is like having if and throw
if (x !== 42) throw new Error
// Or JS developer usually use return
if (x !== 42) return;
Inheritance
Contract can also be inherit just like class. You can access almost everything from Parent contract except specify as private . The way contract inherit is using is keyword instead of extends like in JS class.
contract Doge {}
contract BabyDoge is Doge {}
The different is that contract can inherit from multiple contracts. Not just one. Now that's somehow cool or complicated?
Import
Import file syntax is similar to JS but you don't need to name it (please correct me if I'm wrong).
import "./dogecoin.sol"
contract BabyDoge is DogeCoin {}
Interface
Interface is a way to interact / talk to another contract on the blockchain that we don't own. We can do that because the data is stored openly on the blockchain. But we can only interact with functions that has visibility public or external .
Get The Interface
For example: consider this contract is available in the blockchain.
contract Greeter {
mapping(address => string) myGreet;
function setGreet(string memory _greet) public {
myGreet[msg.sender] = _greet;
}
function getGreet(address _myAddress) public view returns(string) {
return myGreet[_myAddress];
}
}
Now we want to get our special greet in our contract.
contract GreetInterface {
function getGreet(address _myAddress) public view returns(string);
}
That's it. we don't need to declare the whole function that we want to interact with. Only the top line of the function will do.
Using The Interface
Now how to use it? We can use it like this:
// initialize it and save to a variable
GreetInterface theGreet = GreetInterface("how ya doing?");
string favGreet = theGreet.getGreet(msg.sender)
Common Contract
What it means is that this contract pattern is so common that almost most contract use it as its parent contract and inherit their first contract from it (I learn this from cryptozombies ). This contract apply the concept of ownership, so only the owner can call some contract to ensure the security (this is too is ambiguous).
Ownable
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address private _owner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
constructor() internal {
_owner = msg.sender;
emit OwnershipTransferred(address(0), _owner);
}
/**
* @return the address of the owner.
*/
function owner() public view returns(address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(isOwner());
_;
}
/**
* @return true if `msg.sender` is the owner of the contract.
*/
function isOwner() public view returns(bool) {
return msg.sender == _owner;
}
/**
* @dev Allows the current owner to relinquish control of the contract.
* @notice Renouncing to ownership will leave the contract without an owner.
* It will not be possible to call the functions with the `onlyOwner`
* modifier anymore.
*/
function renounceOwnership() public onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
_transferOwnership(newOwner);
}
/**
* @dev Transfers control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function _transferOwnership(address newOwner) internal {
require(newOwner != address(0));
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
Other Topics :
This is the topics that worth looking for that didn't mention in this blog.
- Modifier: we only cover pure and view . You could also create custom modifier.
- Payable.
- Library.
- ERC20.
Other Resource
- Official Solidity Docs .
- Metamask : Chrome Extension Brining Ethereum To Your Browser.
- Truffle : Smart Contract Dev - Testing - Deployment Framework.
- Ganache : Quickly Fire Up A Personal Ethereum Blockchain To Test
- Web3JS : Ethereum JavaScript API Which Connects To Generic JSON RPC
- Infura : Provides Instant & Scalable API Access To The Ethereum & IPFS’s.
- Hardhat : Ethereum development environment for professionals.
- EtherJS : Ethereum Wallet Implementation.
- OpenZeppelin : Implementation Standards For ERC20 & ERC721.
- Etherscan : Block Explorer & Analytics Platform For Ethereum.
Project boilerplate:
- Hardhat boilerplate - Hardhat, Etherjs, React, Bootstrap.
- My custom boilerplate - a fork from Hardhat boilerplate but the frontend built with Vite and support Typescript.