4 - Solidity I.
Tutorial objectives
We will learn Solidity and EVM fundamentals. Additionally, we will implement a simple smart contract representing an ERC-20 token.
Tutorial pre-requisites
- IDE with Solidity support of your choice. VS Code with Solidity (Wake) extension or Remix is recommended.
- VS Code - popular code editor, support for extensions (https://code.visualstudio.com/)
- Solidity (Wake) extension (https://marketplace.visualstudio.com/items?itemName=AckeeBlockchain.tools-for-solidity)
- auto-compilation, syntax highlighting, code navigation, easy deployment
- Remix - All in one, easy to use (https://remix.ethereum.org/)
- auto-compilation, syntax highlighting, easy deployment
- VS Code - popular code editor, support for extensions (https://code.visualstudio.com/)
- ERC-20 standard basics (https://ethereum.org/en/developers/docs/standards/tokens/erc-20/)
Recapitulation
What is Ethereum?
Ethereum is a decentralized platform for building and running decentralized applications. The decentralization is achieved through a P2P network of computers called nodes, where each node maintains a copy of the world state.
While users interact with a single node at a time, the world state is represented and synchronized across all nodes using the blockchain.

Blockchain and World State
Blockchain is a list of blocks, where each block contains:
- A list of executed transactions
- A block header with important metadata (previous block hash, timestamp, hash of the world state)
The blockchain captures the complete history of Ethereum’s world state.
The world state represents the current state of Ethereum at a given point in time. It can be recomputed from the blockchain by executing all transactions from the beginning.

Account Types
Ethereum has two types of accounts:
Externally Owned Accounts (EOA) – user accounts that:
- Can initiate and send transactions
- Do not have code by default*
- Are controlled by private keys
Contract Accounts – smart contract accounts that:
- Cannot initiate transactions (only respond to them)
- Have immutable code and mutable storage
- Are controlled by their code logic
Transactions vs. Calls
Transactions perform changes to the world state. All nodes must execute them, and they cost gas. There are two types:
- Message call transaction – sends data and/or ETH to an address (EOA or contract), typically causing mutable changes (they can be reverted – e.g. sending ETH to a friend, he can send it back to you)
- Contract creation transaction – deploys new contract code to a computed address (immutable change, as code cannot be modified after deployment)
Calls do not change the world state. They only read the current state, are executed only by the receiving node, and are gas-free (not included in the mempool).
Ethereum as World Computer
Ethereum is not only a decentralized network – you can think of Ethereum as a single world computer.
This model makes it easier to understand and use Solidity effectively.
In this model:
- Blockchain = "HDD" – permanent decentralized storage
- History of inputs and outputs – processed transactions and their results
- Only root hash of the structure representing the state is stored (space efficient)
- EVM = "single-core processor & RAM" – transaction-based virtual machine
- Running bytecode – executes the transactions to propose a new block on the blockchain
- Computation limited by gas (Quasi-Turing Complete) – to avoid Denial of Service attacks by infinite loops
- Includes memory for volatile data during computation
- Mempool = "keyboard" – input set of unprocessed transactions
- Solidity = programming language – compiled into bytecode

Simplified execution of message call transaction
When a messsage call transaction is executed on Ethereum, the following steps occur (simplified):
- Transaction is taken from mempool
- Sender and receiver details are loaded from the world state (code of the destination address, balance of the sender)
- Gas is paid upfront before the execution (by the sender), gas limit is set
- Call data is passed to the invoked function
- Function is executed – during execution, stack, memory, and storage are used
- Storage is updated if execution was successful
- Remaining gas is refunded
There are different data locations used during execution. The pink ones (storage, memory, calldata) are used for reference types, while the stack is used for value types. More on this in section Data location of types.
Solidity basics
Solidity provides good language documentation at: https://docs.soliditylang.org/en/latest/.
Documentation should be your first stop when you are looking for something.
The following is a quick overview of the language – it may not be as comprehensive as the documentation, but it should give you a good starting point.
High-level overview
- statically typed, contract-oriented, high-level language for implementing smart contracts on the EVM,
- compiled to EVM bytecode,
- Turing-complete, but not really general purpose, it is a domain-specific language for EVM smart contracts.
Contracts
Solidity is a contract-oriented language. A contract is a collection of code (its functions) and data (its state) that resides at a specific address on the Ethereum blockchain. The contract can be created using the contract
keyword. The following example creates a contract named SimpleBank
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleBank {
uint256 constant internal MIN_DEPOSIT = 1 ether;
bool immutable licensed;
address public owner;
constructor(bool _licensed) {
owner = msg.sender;
licensed = _licensed;
}
}
State variables
State variables are variables whose values are permanently stored in contract storage. Variables declared inside the contract’s body (but outside any function) are state variables. They are expensive to modify and internal by default (bare in mind that internal still means that it is stored on a public blockchain).
In the SimpleBank
contract above, MIN_DEPOSIT
, licensed
, and owner
are state variables:
constant
– value is known at compile time and assigned at declarationimmutable
– value is known at deployment time and assigned in constructor, stored in the codepublic
– automatically generates getter functions (e.g.,owner
can be read by callingowner()
)- By default, state variables are mutable (like
owner
)
The special variable msg.sender
is a globally available variable that holds the address of the function caller (message sender).
Data types
Solidity has several data types, they are divided into value
types and reference
types.
Value types are passed by value – copied when they are used as function arguments or returned from functions.
Reference types can be modified through multiple different names (references). The reference types have to be declared with the data area (if not clear from context) where the type will be stored (memory, storage, calldata).
- Value types
* bool
* uint(8-256), int(8-256)
* address
* enum
* bytes(1-32)
- Reference types
* fixed-size array
* dynamic-size array
* byte array
* string
* struct
* mapping
- An assignment or type conversion that changes the data location will always incur an automatic copy operation, while assignments inside the same data location only copy in some cases for storage types. The rules for copying and referencing are described in https://docs.soliditylang.org/en/latest/types.html#data-location-and-assignment-behaviour.
Data location of types
Data location is a property of a variable that defines where the data is stored.
For reference types, there are 3 data locations:
memory
– volatile, it is erased after the executionstorage
– non-volatile, it is stored on the blockchaincalldata
– read-only and volatile, it is used for function call parameters
Value types are always stored in the stack (unless they are part of a struct or array – then they are reference types).
Most important reference types
The most important reference types are:
- Structs allow users to define new types. They can contain both value and reference types. In our example,
Account
is a struct containing a string and uint256. - Mappings are key-value stores, similar to hash maps in other languages. They can only be declared in storage and they aren’t iterable. Here,
accounts
maps addresses to Account structs. Further information in: https://docs.soliditylang.org/en/latest/types.html#mapping-types. - Arrays can be both static and dynamic. They can be nested -
uint[][5]
- is an array of 5 dynamic arrays of uints. Further information in: https://docs.soliditylang.org/en/latest/types.html#arrays.
Let’s extend our SimpleBank contract to demonstrate some of these:
struct Account {
string name;
uint256 balance;
}
contract SimpleBank {
uint256 constant internal MIN_DEPOSIT = 1 ether;
bool immutable licensed;
address public owner;
mapping(address => Account) public accounts;
constructor(bool _licensed) {
owner = msg.sender;
licensed = _licensed;
}
function createAccount(string calldata name) external {
Account memory newAccount = Account({name: name, balance: 0});
accounts[msg.sender] = newAccount;
}
}
We added struct Account
, which is a new type that can be used to store the account information. It contains a string and a uint256.
We added a mapping accounts
that maps addresses to Account
structs. This allows us to store the account information for each address.
In the createAccount
function above:
name
is copied fromcalldata
into Account struct inmemory
(different location)newAccount
is then copied frommemory
intostorage
when assigned toaccounts[msg.sender]
The msg.sender
here is a special global variable that holds the address of the function caller (message sender). More about it in section [Global accessible variables].
Functions
Functions are the executable units of code and are usually defined inside a contract. Functions can be invoked internally (by moving the program counter) or externally (through a message call). They can be both overloaded and overridden. They can be extended using modifiers, which amend the function’s semantics.
Function visibility
external
– only transactions (or other contracts)public
– transactions/internal callsinternal
– current contract/derived contractsprivate
– current contract only
Function state mutability
pure
– no read/writeview
– read only- not specified (default) – read/write
payable
– read/write & receive/send ether
Let’s add a deposit
function to our SimpleBank:
function deposit() external payable returns (uint256) {
accounts[msg.sender].balance += msg.value;
uint256 x = accounts[msg.sender].balance;
return x;
}
The deposit
function is payable
so it can receive ether. The special variable msg.value
holds how much ETH was sent to the function. The function returns uint256
- the total deposited amount.
Function modifiers
Modifiers are pieces of code that amend the semantics of the function that they modify (similar to decorators in Python). They add additional logic and are often used for validation and access controls. They must have exactly one occurrence of _
(underscore character), which marks where the wrapped function code is executed.
Let’s add a modifier to our SimpleBank to validate deposits:
modifier onlyApprovedValue() {
if (licensed) require(msg.value >= MIN_DEPOSIT, "Too low");
_;
}
function deposit() external payable onlyApprovedValue returns (uint256) {
accounts[msg.sender].balance += msg.value;
uint256 x = accounts[msg.sender].balance;
return x;
}
The onlyApprovedValue
modifier checks if the deposit meets the minimum requirement for licensed banks. Note that when calling a modifier without parameters, you can omit the parentheses.
Globally available
Solidity provides several globally accessible variables that give context about the current transaction and blockchain state and some units.
Variables
These special variables are available in any function without declaration.
Following two identify the different callers in a transaction:
msg.sender
– the immediate caller of the function (changes for each contract in a call chain)tx.origin
– the original external account that initiated the entire transaction (remains the same throughout the entire transaction, even in multi-contract calls)
This distinction is important in multi-contract call chains where one contract calls another.
The msg.sender
and tx.origin
are equal in the first contract in the call chain (the contract that is being called). If this contract calls another contract, the msg.sender
will be the address of the first contract and tx.origin
will be the address of the original external account that initiated the entire transaction.

Other additional variables, that provide context about the execution includes:
msg.value
– the amount of ETH (in wei) sent with the function callmsg.data
– the complete calldata of the function calltx.gasprice
– the gas price of the current transactionblock.number
– the current block numberblock.timestamp
– the current block timestamp- … more in https://docs.soliditylang.org/en/stable/units-and-global-variables.html
Units
Solidity provides several units that are used to represent different quantities of ETH.
wei – 1 wei = 1 = 10^-18 ETH gwei – 1 gwei = 10^9 = 10^-9 ETH ether – 1 ether = 10^18 = 10^18 wei
Time units:
1 == 1 seconds 1 minutes == 60 seconds 1 hours == 60 minutes 1 days == 24 hours 1 weeks == 7 days
Events
Events are an abstraction atop EVM’s logging functionality. Smart contracts should emit events when they make important state changes. Off-chain components can subscribe to the events and react to them – for example, a frontend application can display changes of state to a user.
Events are declared using the event
keyword and can have indexed
parameters to enable filtering. Let’s add events to our SimpleBank:
event AccountCreated(address indexed account, string name);
event Deposit(address indexed account, uint256 amount);
function createAccount(string calldata name) external {
Account memory newAccount = Account({name: name, balance: 0});
accounts[msg.sender] = newAccount;
emit AccountCreated(msg.sender, name);
}
function deposit() external payable onlyApprovedValue returns (uint256) {
accounts[msg.sender].balance += msg.value;
uint256 x = accounts[msg.sender].balance;
emit Deposit(msg.sender, msg.value);
return x;
}
Errors and revert statements
Transactions may not always end with a successful state transition. Reasons for failure include wrong inputs, insufficient funds, or access control violations. When an error occurs, the transaction is reverted:
- All state changes are reverted
- The reason is explained to the caller
- Gas consumed up to the failure point is still paid
Errors are declared using the error
keyword and can have parameters (similar to events). Let’s add custom errors to our SimpleBank:
error DepositTooLow();
modifier onlyApprovedValue() {
if (licensed) require(msg.value >= MIN_DEPOSIT, DepositTooLow());
_;
}
require
vs. revert
Revert statements: The statement require(condition, "description");
is equivalent to if (!condition) revert Error("description")
. Both are convenient ways to check conditions and revert the execution if the condition is not met.
- Custom errors (with
error
keyword) can have parameters and are more gas-efficient require
can now use custom errors (since Solidity 0.8.26)revert
is typically paired with conditions and produces errors when the condition is true
Error vs. Panic
Error
– "regular" error conditions that should be handledPanic
– should not occur in bug-free code (e.g., division by zero, integer overflow/underflow, array out of bounds)assert
– for checking invariants and debugging
Creating a smart contract: ERC-20 token
Ethereum has its native token - Ether. Ether is used to pay for the execution of smart contracts. Ether is also used to pay for the storage of data on the Ethereum blockchain. It is directly incorporated into the protocol and the balances are directly stored in the Account state.
However, we can create smart contracts and thus we can develop a new token atop Ethereum. The smart contract will provide the state for the token (eg the balances of the token holders) and the logic for the token (eg the transfer function).
We will create a simple ERC-20 token. ERC-20 is a standard for tokens on Ethereum. It defines a set of functions that a token contract has to implement. The standard is defined at https://eips.ethereum.org/EIPS/eip-20. Because the tokens have their standard they can be easily integrated into other smart contracts. For example, a decentralized exchange can integrate a token and then simply call the transfer function of the token contract.
Create a new contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
contract HelloToken {
// TODO
}
Variables
Name of your token.
string private _name;
Symbol of your token.
string private _symbol;
Number of decimals. Decimals are usually set to 18 to mimic the properties of Ether. This means that 1 token is 1 * 10^18.
uint8 private _decimals;
Total supply of your token.
uint256 private _totalSupply;
Mapping, which holds the account balances, eg. _balances[alice] = 10.
mapping (address => uint256) private _balances;
Mapping, which holds transfer allowances from a sender to a recipient. The recipient can spend the allowance on behalf of the sender.
mapping (address => mapping (address => uint256)) private _allowances;
Events
Define events for logging the state transitions.
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
Constructor
Is called during the contract deployment. It initializes the state of the contract and produces the final bytecode that gets deployed to the network.
constructor () public {
_name = "HelloToken";
_symbol = "HELLO";
_decimals = 18;
}
Getter functions
function name() public view returns (string memory) {
return _name;
}
function symbol() public view returns (string memory) {
return _symbol;
}
function decimals() public view returns (uint8) {
return _decimals;
}
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
Token balance of the specific address.
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
Supply functions
Mints new tokens specified by the amount to the specified address. Increases the total supply.
function _mint(address account, uint256 amount) internal {
_totalSupply += amount;
_balances[account] += amount;
emit Transfer(address(0), account, amount);
}
Burns amount of the token from the specified address, decreases the total supply.
function _burn(address account, uint256 amount) internal {
require(_balances[account] >= amount, "ERC20: burn amount exceeds balance");
_balances[account] -= amount;
_totalSupply -= amount;
emit Transfer(account, address(0), amount);
}
Now you can add this line to your constructor. It will mint fixed amount of the token during the deployment to the deployer’s address.
_mint(msg.sender, 1000 * 10 ** _decimals);
Allowance functions
Get the allowance amount from the owner to a spender.
function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}
Set the allowance amount to the spender.
function approve(address spender, uint256 amount) public returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
Transfer functions
Transfers the specified amount from the owner to the recipient.
function transferFrom(address owner, address recipient, uint256 amount) public returns (bool) {
require(allowance(owner, msg.sender) >= amount, "ERC20: transfer amount exceeds allowance");
require(_balances[owner] >= amount, "ERC20: transfer amount exceeds balance");
_balances[owner] -= amount;
_balances[recipient] += amount;
_allowances[owner][msg.sender] -= amount;
emit Transfer(owner, recipient, amount);
return true;
}
Transfers the specified amount to the recipient.
function transfer(address recipient, uint256 amount) public returns (bool) {
return transferFrom(msg.sender, recipient, amount);
}
Compile, deploy & interact
Compile your contract, deploy it locally to a Javascript VM or the Ganache (Web 3 Provider in the Remix IDE). Transfer some tokens from address A to address B.
Optional homework
If you want to practice Solidity more, you can implement NFT (Non-Fungible Token) using ERC-721 standard. (https://ethereum.org/en/developers/docs/standards/tokens/erc-721/)
Recommended reading and resources
The following resources were used for creating this tutorial. The italicized text is slightly modified text from those sources. They are recommended for further reading.
- Solidity documentation (https://docs.soliditylang.org/en/stable/)
- official Solidity documentation
- Mastering Ethereum, chapter on EVM (https://github.com/ethereumbook/ethereumbook/blob/develop/13evm.asciidoc)
- free book about Ethereum with a good chapter on EVM
- Ethereum Yellow Paper (https://ethereum.github.io/yellowpaper/paper.pdf)
- official Ethereum specification
- More accessible interpretation of the Yellow Paper (https://ethereum.org/en/developers/tutorials/yellow-paper-evm/#main-content)
- Video tutorial on the Yellow Paper (https://youtu.be/e84V1MxRlYs)
- EVM Opcodes (https://www.evm.codes/)
- interactive EVM opcodes reference
- Introduction to smart contracts https://docs.soliditylang.org/en/latest/introduction-to-smart-contracts.html#introduction-to-smart-contracts)
- introduction to smart contracts from solidity documentation