TechLead
Lesson 4 of 20
6 min read
Web3 & Blockchain

Solidity Programming Basics

Learn the Solidity programming language for writing smart contracts — types, functions, control flow, modifiers, and inheritance

What is Solidity?

Solidity is a statically-typed, contract-oriented programming language designed for writing smart contracts on the Ethereum Virtual Machine (EVM). Its syntax is influenced by C++, Python, and JavaScript, making it approachable for developers coming from those backgrounds. Solidity compiles to EVM bytecode, which is then deployed to and executed on the blockchain.

Solidity Key Features

  • Static Typing: Variables must be declared with types, catching errors at compile time
  • Inheritance: Contracts can inherit from other contracts, enabling code reuse
  • Libraries: Reusable code that can be deployed once and called by many contracts
  • Custom Modifiers: Reusable function guards for access control and validation
  • Events: First-class logging mechanism for off-chain communication

Data Types

Solidity provides a rich type system. Understanding these types and their gas costs is essential for writing efficient contracts.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract DataTypes {
    // Value Types
    bool public isActive = true;
    uint256 public totalSupply = 1000000;    // Unsigned integer (0 to 2^256-1)
    int256 public temperature = -10;          // Signed integer
    address public owner = msg.sender;        // 20-byte Ethereum address
    bytes32 public dataHash;                  // Fixed-size byte array

    // Address with payable — can receive ETH
    address payable public treasury;

    // Enums — user-defined type with named constants
    enum Status { Pending, Active, Closed }
    Status public currentStatus = Status.Pending;

    // Reference Types
    string public name = "My Token";          // Dynamic string (UTF-8)
    bytes public rawData;                     // Dynamic byte array

    // Arrays
    uint256[] public dynamicArray;            // Dynamic-size array
    uint256[10] public fixedArray;            // Fixed-size array

    // Mappings — hash table for key-value pairs
    mapping(address => uint256) public balances;
    mapping(address => mapping(address => uint256)) public allowances; // Nested

    // Structs — custom data structures
    struct User {
        address wallet;
        string username;
        uint256 balance;
        bool isVerified;
    }

    mapping(address => User) public users;

    // Constants and Immutables — gas optimizations
    uint256 public constant MAX_SUPPLY = 10_000_000;  // Set at compile time
    uint256 public immutable deployTime;               // Set once in constructor

    constructor() {
        deployTime = block.timestamp;
        treasury = payable(msg.sender);
    }
}

Functions and Visibility

Functions in Solidity have visibility specifiers that control who can call them, and state mutability keywords that indicate how they interact with the blockchain state.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Functions {
    uint256 private value;
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // external — only callable from outside the contract
    function setValue(uint256 _value) external {
        value = _value;
    }

    // public — callable both externally and internally
    function getValue() public view returns (uint256) {
        return value;
    }

    // internal — only this contract and derived contracts
    function _doubleValue() internal view returns (uint256) {
        return value * 2;
    }

    // private — only this contract (not even derived)
    function _validate(uint256 _val) private pure returns (bool) {
        return _val > 0 && _val < 1000000;
    }

    // view — reads state but does not modify it (no gas when called externally)
    function getDoubled() external view returns (uint256) {
        return _doubleValue();
    }

    // pure — does not read or modify state
    function add(uint256 a, uint256 b) external pure returns (uint256) {
        return a + b;
    }

    // payable — can receive ETH with the call
    function deposit() external payable {
        require(msg.value > 0, "Must send ETH");
    }

    // Multiple return values
    function getInfo() external view returns (uint256, address, bool) {
        return (value, owner, value > 0);
    }
}

Control Flow and Error Handling

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract ControlFlow {
    // Custom errors (gas-efficient since Solidity 0.8.4)
    error InsufficientBalance(uint256 available, uint256 required);
    error Unauthorized(address caller);

    mapping(address => uint256) public balances;
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function transfer(address to, uint256 amount) external {
        // require — validates input/state, reverts with message
        require(to != address(0), "Cannot transfer to zero address");

        // Custom error — more gas efficient than string messages
        if (balances[msg.sender] < amount) {
            revert InsufficientBalance(balances[msg.sender], amount);
        }

        // If-else
        if (amount > 1000) {
            // Large transfer logic
        } else if (amount > 0) {
            // Normal transfer logic
        } else {
            revert("Amount must be positive");
        }

        balances[msg.sender] -= amount;
        balances[to] += amount;
    }

    // For loops (be careful with gas!)
    function sumArray(uint256[] calldata arr) external pure returns (uint256) {
        uint256 total = 0;
        for (uint256 i = 0; i < arr.length; i++) {
            total += arr[i];
        }
        return total;
    }

    // While loop
    function findIndex(uint256[] calldata arr, uint256 target)
        external pure returns (int256)
    {
        uint256 i = 0;
        while (i < arr.length) {
            if (arr[i] == target) return int256(i);
            i++;
        }
        return -1;
    }

    // assert — check for internal errors / invariants
    function withdraw(uint256 amount) external {
        uint256 previousBalance = balances[msg.sender];
        balances[msg.sender] -= amount;

        // This should always hold — if it doesn't, something is very wrong
        assert(balances[msg.sender] <= previousBalance);
    }

    // try/catch for external calls
    function safeCall(address target) external returns (bool) {
        try IExternal(target).doSomething() returns (uint256 result) {
            // Handle success
            return true;
        } catch Error(string memory reason) {
            // Handle require/revert with message
            return false;
        } catch (bytes memory) {
            // Handle low-level errors
            return false;
        }
    }
}

interface IExternal {
    function doSomething() external returns (uint256);
}

Modifiers and Inheritance

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// Base contract with access control
abstract contract Ownable {
    address public owner;

    event OwnershipTransferred(address indexed previous, address indexed next);

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _; // Placeholder for the function body
    }

    constructor() {
        owner = msg.sender;
    }

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "Invalid address");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
}

// Pausable adds pause/unpause functionality
abstract contract Pausable is Ownable {
    bool public paused;

    event Paused(address account);
    event Unpaused(address account);

    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }

    function pause() external onlyOwner {
        paused = true;
        emit Paused(msg.sender);
    }

    function unpause() external onlyOwner {
        paused = false;
        emit Unpaused(msg.sender);
    }
}

// Final contract inherits both Ownable and Pausable
contract MyToken is Pausable {
    mapping(address => uint256) public balances;
    uint256 public totalSupply;

    event Transfer(address indexed from, address indexed to, uint256 amount);

    // Combining modifiers — both must pass
    function transfer(address to, uint256 amount)
        external
        whenNotPaused
    {
        require(balances[msg.sender] >= amount, "Insufficient balance");
        balances[msg.sender] -= amount;
        balances[to] += amount;
        emit Transfer(msg.sender, to, amount);
    }

    // onlyOwner inherited from Ownable through Pausable
    function mint(address to, uint256 amount) external onlyOwner {
        totalSupply += amount;
        balances[to] += amount;
        emit Transfer(address(0), to, amount);
    }
}

Interfaces and Abstract Contracts

Interfaces define the external API of a contract without implementation. They are essential for interacting with other deployed contracts and for defining standards like ERC-20 and ERC-721. Abstract contracts can contain some implementation but leave certain functions unimplemented for child contracts to define.

Best Practices for Solidity

  • Use latest compiler: Always use Solidity 0.8.x or later for built-in overflow checks
  • Custom errors over strings: They save gas and provide structured error data
  • Use calldata for read-only params: Cheaper than memory for external function parameters
  • Mark functions correctly: Use view/pure when possible; use external over public when the function is not called internally
  • Avoid unbounded loops: Gas limits can cause transactions to fail if loops iterate too many times
  • Use events for tracking: Emit events for all important state changes to enable off-chain indexing

Continue Learning