Module 2: Solidity Basics
Introduction
Welcome to Module 2 of our Solidity programming course! In this module, we'll dive into the basics of Solidity, the primary programming language for Ethereum smart contracts. We'll explore the fundamental syntax, data types, and structures that form the building blocks of smart contract development.
Learning Objectives
By the end of this module, you will be able to:
- Write and understand basic Solidity code
- Create a simple smart contract
- Use various data types and variables in Solidity
- Implement functions with different visibility specifiers
- Apply control structures like conditionals and loops
- Understand the basics of gas and optimization
1. Your First Smart Contract
1.1 Solidity File Structure
Solidity files have the .sol extension. Let's start with a simple "Hello World" contract:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract HelloWorld {
string public message;
constructor() {
message = "Hello, World!";
}
function updateMessage(string memory newMessage) public {
message = newMessage;
}
}
Let's break down this contract:
- SPDX License Identifier: A comment that specifies the license under which the code is released.
- Pragma Directive: Specifies the compiler version to use.
^0.8.17means "use version 0.8.17 or any compatible version above it but below 0.9.0". - Contract Declaration: Similar to a class in object-oriented programming, defines a new contract named "HelloWorld".
- State Variable:
messageis a state variable that persists in contract storage. - Constructor: Executed only once when the contract is deployed.
- Function:
updateMessageallows changing the message.
1.2 Compiling and Deploying
To compile and deploy this contract, you would typically:
- Use a development environment like Remix, Hardhat, or Truffle
- Compile the contract to bytecode
- Deploy the bytecode to an Ethereum network (mainnet, testnet, or local development network)
We'll cover the development environment in detail in Module 3.
2. Solidity Data Types
2.1 Value Types
Value types are copied when they are used as function arguments or in assignments.
Boolean
bool public isActive = true;
bool public isComplete = false;
Integers
// Unsigned integers (non-negative only)
uint8 public smallNumber = 255; // 8 bits, range: 0 to 2^8-1
uint16 public mediumNumber = 65535; // 16 bits
uint256 public bigNumber = 115792089237316195423570985008687907853269984665640564039457584007913129639935; // 256 bits (default)
uint public defaultUint = 123; // uint is an alias for uint256
// Signed integers (can be negative)
int8 public smallSignedNumber = -128; // 8 bits, range: -2^7 to 2^7-1
int256 public bigSignedNumber = -57896044618658097711785492504343953926634992332820282019728792003956564819968; // 256 bits
int public defaultInt = -456; // int is an alias for int256
Address
address public userAddress = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
address payable public payableAddress = payable(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4); // Can receive Ether
Bytes and Strings
bytes1 public singleByte = 0x61; // Represents 'a' in ASCII
bytes public dynamicBytes = "abc"; // Dynamic array of bytes
string public text = "Hello, Solidity!"; // UTF-8 encoded string
Enums
enum Status { Pending, Active, Completed, Cancelled }
Status public currentStatus = Status.Pending;
function activateStatus() public {
currentStatus = Status.Active;
}
2.2 Reference Types
Reference types can be modified through multiple variable names (similar to pointers in other languages).
Arrays
// Fixed-size array
uint[5] public fixedArray = [1, 2, 3, 4, 5];
// Dynamic array
uint[] public dynamicArray;
function addToArray(uint value) public {
dynamicArray.push(value);
}
function getArrayLength() public view returns (uint) {
return dynamicArray.length;
}
Mappings
// Mapping from address to uint
mapping(address => uint) public balances;
function updateBalance(uint newBalance) public {
balances[msg.sender] = newBalance;
}
Structs
struct User {
string name;
uint age;
address wallet;
}
User public admin = User("Admin", 30, msg.sender);
mapping(address => User) public users;
function registerUser(string memory name, uint age) public {
users[msg.sender] = User(name, age, msg.sender);
}
3. Variables in Solidity
3.1 State Variables
State variables are permanently stored in contract storage. They are declared at the contract level.
contract StateVariablesExample {
// State variables
uint public stateVar1 = 100;
address public owner;
bool public isActive;
constructor() {
owner = msg.sender;
isActive = true;
}
}
3.2 Local Variables
Local variables are only available within the function where they are defined. They are not stored on the blockchain.
function calculateSum(uint a, uint b) public pure returns (uint) {
// Local variable
uint result = a + b;
return result;
}
3.3 Global Variables
Solidity provides special global variables that are available in all functions. These provide information about the blockchain and the current transaction.
function getTransactionInfo() public view returns (address, uint, uint) {
return (
msg.sender, // Address that called the function
block.number, // Current block number
block.timestamp // Current block timestamp
);
}
Common global variables include:
msg.sender: Address that called the functionmsg.value: Amount of Ether sent with the function call (in wei)block.number: Current block numberblock.timestamp: Current block timestamp (in seconds since Unix epoch)tx.gasprice: Gas price of the transaction
4. Functions in Solidity
4.1 Function Declaration
The basic syntax for declaring a function in Solidity is:
function functionName(parameterType parameterName) visibility [state mutability] [returns (returnType)] {
// Function body
}
4.2 Function Visibility
Solidity provides four visibility specifiers for functions:
- public: Accessible from within the contract, derived contracts, and externally
- private: Only accessible from within the current contract
- internal: Accessible from within the current contract and derived contracts
- external: Only accessible externally (not from within the contract)
contract VisibilityExample {
// Public function - can be called from anywhere
function publicFunction() public pure returns (string memory) {
return "This is public";
}
// Private function - can only be called from within this contract
function privateFunction() private pure returns (string memory) {
return "This is private";
}
// Internal function - can be called from this contract and derived contracts
function internalFunction() internal pure returns (string memory) {
return "This is internal";
}
// External function - can only be called from outside this contract
function externalFunction() external pure returns (string memory) {
return "This is external";
}
// Function that calls the private function
function callPrivateFunction() public pure returns (string memory) {
return privateFunction();
}
}
4.3 State Mutability
State mutability specifiers indicate whether a function modifies the state:
- view: Function does not modify the state (but can read from it)
- pure: Function neither modifies nor reads from the state
- payable: Function can receive Ether
contract StateMutabilityExample {
uint public value;
// Function that modifies state
function setValue(uint newValue) public {
value = newValue;
}
// View function - reads but doesn't modify state
function getValue() public view returns (uint) {
return value;
}
// Pure function - neither reads nor modifies state
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
// Payable function - can receive Ether
function deposit() public payable {
// The function body can be empty
}
// Function to check contract balance
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
4.4 Function Modifiers
Modifiers are used to change the behavior of functions in a declarative way. They are commonly used for access control.
contract ModifierExample {
address public owner;
constructor() {
owner = msg.sender;
}
// Modifier to check if caller is owner
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_; // Continue execution
}
// Function that uses the modifier
function changeOwner(address newOwner) public onlyOwner {
owner = newOwner;
}
}
4.5 Function Overloading
Solidity supports function overloading, which means you can have multiple functions with the same name but different parameter types.
contract OverloadingExample {
// Function with uint parameter
function getValue(uint id) public pure returns (uint) {
return id;
}
// Overloaded function with string parameter
function getValue(string memory name) public pure returns (string memory) {
return name;
}
}
5. Control Structures
5.1 Conditionals
Solidity supports standard conditional statements:
function checkValue(uint value) public pure returns (string memory) {
if (value > 100) {
return "Greater than 100";
} else if (value == 100) {
return "Equal to 100";
} else {
return "Less than 100";
}
}
5.2 Loops
Solidity supports for, while, and do-while loops:
// For loop
function sumArray(uint[] memory numbers) public pure returns (uint) {
uint sum = 0;
for (uint i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}
// While loop
function factorial(uint n) public pure returns (uint) {
uint result = 1;
while (n > 1) {
result *= n;
n--;
}
return result;
}
// Do-while loop
function findFirstDivisor(uint n) public pure returns (uint) {
uint i = 2;
do {
if (n % i == 0) {
return i;
}
i++;
} while (i < n);
return n;
}
⚠️ Warning: Gas Considerations with Loops
Be cautious when using loops in Solidity. If the number of iterations is unbounded or very large, the function might exceed the block gas limit and fail. Always ensure loops have a reasonable upper bound.
6. Error Handling
6.1 Require, Assert, and Revert
Solidity provides three functions for error handling:
- require: Used for input validation and checking conditions that should be met
- assert: Used for checking invariants and conditions that should never be false
- revert: Used to flag an error and revert the current call
contract ErrorHandlingExample {
mapping(address => uint) public balances;
// Using require
function withdraw(uint amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
}
// Using assert
function transfer(address to, uint amount) public {
uint oldSenderBalance = balances[msg.sender];
uint oldReceiverBalance = balances[to];
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
// Assert that the total balance remains the same
assert(balances[msg.sender] + balances[to] == oldSenderBalance + oldReceiverBalance);
}
// Using revert
function deposit(uint amount) public {
if (amount == 0) {
revert("Cannot deposit zero amount");
}
balances[msg.sender] += amount;
}
}
6.2 Custom Errors
Solidity 0.8.4 introduced custom errors, which are more gas-efficient than string error messages:
contract CustomErrorExample {
// Define custom errors
error InsufficientBalance(address user, uint balance, uint required);
error ZeroAmount();
mapping(address => uint) public balances;
function withdraw(uint amount) public {
if (balances[msg.sender] < amount) {
revert InsufficientBalance(msg.sender, balances[msg.sender], amount);
}
balances[msg.sender] -= amount;
}
function deposit(uint amount) public {
if (amount == 0) {
revert ZeroAmount();
}
balances[msg.sender] += amount;
}
}
7. Events
Events in Solidity allow contracts to communicate with the outside world. They are especially useful for logging and for triggering UI updates in decentralized applications.
contract EventExample {
// Define an event
event Transfer(address indexed from, address indexed to, uint amount);
mapping(address => uint) public balances;
function transfer(address to, uint amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
balances[to] += amount;
// Emit the event
emit Transfer(msg.sender, to, amount);
}
}
The indexed keyword allows for efficient filtering of events. Up to three parameters can be indexed.
8. Gas and Optimization Basics
8.1 Understanding Gas
Gas is the unit that measures the computational effort required to execute operations on the Ethereum network. Every operation in Solidity costs a certain amount of gas.
The total gas cost of a transaction is calculated as:
Total Gas Cost = Gas Used × Gas Price
Where:
- Gas Used: The amount of computational work required
- Gas Price: The amount of Ether the sender is willing to pay per unit of gas (in wei)
8.2 Basic Gas Optimization Tips
- Use appropriate data types: Use the smallest data type that can hold your values (e.g., uint8 instead of uint256 if possible)
- Batch operations: Combine multiple operations into a single transaction
- Minimize storage usage: Storage operations are expensive; use memory for temporary data
- Avoid loops with unbounded iterations: They can lead to out-of-gas errors
- Use events for logging: Cheaper than storing data in contract storage
9. Putting It All Together: A Simple Token Contract
Let's create a simple token contract that incorporates many of the concepts we've learned:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
contract SimpleToken {
// State variables
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
// Mapping of address to token balance
mapping(address => uint256) public balanceOf;
// Mapping of address to mapping of address to allowance
mapping(address => mapping(address => uint256)) public allowance;
// Events
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// Custom errors
error InsufficientBalance(address from, uint256 balance, uint256 amount);
error InsufficientAllowance(address owner, address spender, uint256 allowed, uint256 amount);
// Constructor
constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
decimals = _decimals;
// Calculate total supply with decimal consideration
totalSupply = _initialSupply * 10**uint256(decimals);
// Assign all tokens to the contract creator
balanceOf[msg.sender] = totalSupply;
// Emit transfer event from address(0) to creator
emit Transfer(address(0), msg.sender, totalSupply);
}
// Transfer tokens from sender to recipient
function transfer(address to, uint256 amount) public returns (bool) {
if (balanceOf[msg.sender] < amount) {
revert InsufficientBalance(msg.sender, balanceOf[msg.sender], amount);
}
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
// Approve spender to spend tokens on behalf of owner
function approve(address spender, uint256 amount) public returns (bool) {
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
// Transfer tokens from one address to another (with allowance)
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
if (balanceOf[from] < amount) {
revert InsufficientBalance(from, balanceOf[from], amount);
}
if (allowance[from][msg.sender] < amount) {
revert InsufficientAllowance(from, msg.sender, allowance[from][msg.sender], amount);
}
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}
This contract implements a basic ERC-20 like token with the following features:
- Token metadata (name, symbol, decimals)
- Balance tracking for each address
- Transfer functionality
- Approval and transferFrom for delegated transfers
- Events for transfers and approvals
- Custom errors for better error handling
Summary
In this module, we've covered the basics of Solidity programming, including:
- Creating a simple smart contract
- Understanding Solidity data types
- Working with variables (state, local, and global)
- Implementing functions with different visibility and state mutability
- Using control structures like conditionals and loops
- Handling errors with require, assert, revert, and custom errors
- Emitting events for external communication
- Basic gas optimization techniques
- Creating a simple token contract
With these fundamentals, you're now ready to explore more complex Solidity concepts and start building your own smart contracts.
Exercises
- Modify the SimpleToken contract to add a function that allows the owner to mint new tokens.
- Create a voting contract where users can vote for candidates and the contract keeps track of vote counts.
- Implement a time-locked wallet that only allows withdrawals after a specified time period.
- Create a contract that implements a simple auction mechanism.