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:

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:

1.2 Compiling and Deploying

To compile and deploy this contract, you would typically:

  1. Use a development environment like Remix, Hardhat, or Truffle
  2. Compile the contract to bytecode
  3. 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:

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:

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:

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:

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:

8.2 Basic Gas Optimization Tips

  1. Use appropriate data types: Use the smallest data type that can hold your values (e.g., uint8 instead of uint256 if possible)
  2. Batch operations: Combine multiple operations into a single transaction
  3. Minimize storage usage: Storage operations are expensive; use memory for temporary data
  4. Avoid loops with unbounded iterations: They can lead to out-of-gas errors
  5. 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:

Summary

In this module, we've covered the basics of Solidity programming, including:

With these fundamentals, you're now ready to explore more complex Solidity concepts and start building your own smart contracts.

Exercises

  1. Modify the SimpleToken contract to add a function that allows the owner to mint new tokens.
  2. Create a voting contract where users can vote for candidates and the contract keeps track of vote counts.
  3. Implement a time-locked wallet that only allows withdrawals after a specified time period.
  4. Create a contract that implements a simple auction mechanism.

Further Reading