Unit 5: Solidity

CSC307 — Blockchain Architecture And Design-I 7 min read

Solidity is a statically typed, contract-oriented language (released 2015, authored largely by Gavin Wood, Christian Reitwiessner and the Ethereum team) that compiles to bytecode run by the Ethereum Virtual Machine (EVM). Programs are deployed as smart contracts: self-contained accounts holding code and state that execute deterministically when transactions call them. This section fixes the vocabulary the rest of the unit relies on.

  • Compilation target: Solidity source (.sol) compiles to EVM bytecode plus an ABI (Application Binary Interface) that describes callable functions.
  • Deterministic execution: every node runs identical bytecode and must reach identical state, so sources of randomness and external I/O are forbidden.
  • Gas: each EVM operation costs gas, paid in ether; a transaction reverts if it exceeds the supplied gas limit.
  • Account types: externally owned accounts (controlled by private keys) and contract accounts (controlled by code).
  • State persistence: storage variables live permanently on-chain; memory and stack values exist only during a call.
  • Immutability: once deployed, bytecode cannot change, which forces attention to correctness before deployment.

II. Installing Remix IDE

The browser-based toolchain for writing, compiling and deploying contracts.

Remix is the reference IDE for Solidity, requiring no local installation because it runs entirely in the browser at remix.ethereum.org.

A. Installing Remix IDE (online Solidity compiler and development environment)

Remix lowers the entry barrier by bundling an editor, compiler and deployment tools into one web page.

  • Access without install: open https://remix.ethereum.org; the application loads as a single-page web app, so no package manager or runtime is needed locally.
  • File Explorer: create a workspace and add a file such as Token.sol; Remix stores files in browser local storage by default.
  • Solidity Compiler plugin: choose a compiler version (e.g. 0.8.24), enable optimization, and press Compile; errors and warnings appear inline with line numbers.
  • Deploy & Run Transactions plugin: three environments are offered:
    • Remix VM: an in-browser simulated chain with pre-funded test accounts, ideal for fast iteration.
    • Injected Provider: connects to MetaMask to deploy on testnets (e.g. Sepolia) or mainnet.
    • External HTTP provider: connects to a local node such as Ganache or Hardhat on http://127.0.0.1:8545.
  • Desktop option: Remix is also available as a downloadable Desktop IDE and via the @remix-project/remixd daemon, which bridges the browser to a local folder for persistent file access.

III. Token Standards

Shared interfaces that let wallets and exchanges treat different tokens uniformly.

A token standard is an ERC (Ethereum Request for Comments) specification defining the minimum functions and events a contract must expose so external tools can interact with it predictably.

A. ERC-20 — the fungible token standard

ERC-20 (EIP-20, 2015) describes interchangeable tokens where every unit is identical in value, the model for most cryptocurrencies and governance tokens.

  • Mandatory functions: the interface a compliant contract must implement:
    SOLIDITY
      function totalSupply() external view returns (uint256);
      function balanceOf(address owner) external view returns (uint256);
      function transfer(address to, uint256 amount) external returns (bool);
      function approve(address spender, uint256 amount) external returns (bool);
      function allowance(address owner, address spender) external view returns (uint256);
      function transferFrom(address from, address to, uint256 amount) external returns (bool);
  • Events: Transfer(from, to, value) and Approval(owner, spender, value) are emitted so off-chain indexers can track balances.
  • Approve/transferFrom pattern: approve sets an allowance a third party (e.g. a DEX contract) may later spend via transferFrom, enabling delegated transfers without exposing keys.
  • Decimals convention: decimals() (usually 18) means balances are stored as integers; 1 token equals 10**18 base units, avoiding floating point.

B. ERC-721 — the non-fungible token standard

ERC-721 (EIP-721, 2018) describes unique, non-interchangeable tokens, each identified by a distinct uint256 tokenId, used for collectibles and digital deeds.

  • Ownership mapping: every token has exactly one owner at a time, tracked by ownerOf(tokenId); balanceOf(owner) returns a count, not a divisible amount.
  • Core functions:
    SOLIDITY
      function ownerOf(uint256 tokenId) external view returns (address);
      function safeTransferFrom(address from, address to, uint256 tokenId) external;
      function approve(address to, uint256 tokenId) external;
      function setApprovalForAll(address operator, bool approved) external;
  • Metadata extension: tokenURI(tokenId) returns a URI (often IPFS) pointing to JSON describing name, image and attributes.
  • Contrast with ERC-20:
    1. ERC-20: fungible, transfer moves an amount; two holdings of equal size are indistinguishable.
    2. ERC-721: non-fungible, transfers move a specific tokenId; no two tokens are equivalent.

IV. Solidity Programming

The syntax and semantics for expressing contract logic.

Solidity code is organised into contracts, which group state variables, functions and events much like classes group fields and methods.

A. Solidity programming

A contract declares its compiler version, then its state and behaviour.

  • File header: every file begins with a licence identifier and pragma:
    SOLIDITY
      // SPDX-License-Identifier: MIT
      pragma solidity ^0.8.24;
  • Value types: uint256, int256, bool, address, bytes32, and fixed-size enum; since 0.8.0 arithmetic reverts on overflow by default.
  • Reference types: string, dynamic arrays, struct, and mapping(keyType => valueType) for key-value storage.
  • Function visibility and mutability:
    • public / external: callable from outside; internal / private restrict access.
    • view: reads but does not modify state; pure: touches no state; payable: may receive ether.
  • Modifiers: reusable precondition checks, e.g. modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; } where _ marks where the body executes.
  • Global variables: msg.sender (caller), msg.value (ether sent), block.timestamp, used for access control and accounting.
  • Minimal worked example — a counter contract:
    SOLIDITY
      contract Counter {
          uint256 public count;              // state in storage
          event Incremented(uint256 newCount);
    
          function increment() external {
              count += 1;                    // reverts on overflow
              emit Incremented(count);
          }
      }

    Here count persists on-chain, increment mutates it, and the event logs each change for off-chain listeners.

V. Smart Contract Management

Deploying, upgrading and controlling contracts over their lifecycle.

Because deployed bytecode is immutable, management focuses on access control, lifecycle events, and planned upgradeability.

A. Smart contract management

Managing a contract means governing who may act, how funds move, and how logic can evolve.

  • Constructor and deployment: the constructor runs once at deployment to set initial state such as owner = msg.sender; its code is not stored on-chain afterwards.
  • Access control: restrict sensitive functions with an owner pattern or role-based control (e.g. OpenZeppelin Ownable, AccessControl), preventing unauthorised minting or withdrawal.
  • Error handling: require(condition, "msg") validates inputs and refunds remaining gas; revert CustomError() saves gas versus string messages; assert guards invariants.
  • Events as audit trail: emitting events (emit Transfer(...)) is the standard way to record state changes cheaply for front ends and analytics.
  • Upgradeability: since code cannot be edited, upgrade via the proxy pattern — a proxy contract holds state and delegatecalls to a replaceable logic contract, so the address and data stay fixed while behaviour changes.
  • Library reuse: importing audited libraries (import "@openzeppelin/contracts/token/ERC20/ERC20.sol";) reduces bugs by inheriting tested implementations.
  • Gas and cost management: minimise storage writes (the costliest operation), pack variables into fewer slots, and prefer calldata over memory for external function arguments.

VI. User Interface

Connecting a web front end to deployed contracts.

A smart contract is only usable when an interface lets non-technical users send transactions and read state through a wallet.

A. User interface

The front end communicates with the blockchain using a JavaScript library and a browser wallet that signs transactions.

  • Wallet bridge: MetaMask injects a provider at window.ethereum; the dApp requests account access with eth_requestAccounts before transacting.
  • JavaScript libraries: ethers.js or web3.js wrap JSON-RPC calls; a contract object is built from its address and ABI.
  • Instantiating a contract:
    JAVASCRIPT
      const provider = new ethers.BrowserProvider(window.ethereum);
      const signer   = await provider.getSigner();
      const contract = new ethers.Contract(address, abi, signer);

    The ABI tells the library how to encode function calls into transaction data.
  • Reads versus writes:
    1. Calls (reads): view/pure functions like balanceOf run locally, return instantly and cost no gas.
    2. Transactions (writes): state-changing calls like transfer are signed by the wallet, broadcast, mined, and cost gas; the UI awaits the receipt before confirming.
  • Event subscription: the UI listens with contract.on("Transfer", handler) to update balances in real time without polling.
  • Network awareness: the interface should detect chainId, prompt the user to switch networks if it mismatches the deployment, and display pending/confirmed transaction status to manage expectations during block confirmation.