Smart Contracts

Chapter 5
Solidity Programming Fundamentals

Diploma in Blockchain
Smart Contracts   •   Diploma

Solidity Programming Fundamentals

Introduction

Solidity is a statically typed, contract-oriented programming language designed primarily for Ethereum-compatible blockchains. A Solidity program defines executable rules whose results are recorded in blockchain state. Unlike conventional software, a deployed contract cannot normally be edited in place, execution consumes gas, and interactions may be initiated by unknown or adversarial users. These characteristics make clarity, type safety, access control, and careful data management essential.

This chapter develops the language foundations required to write practical Solidity contracts. It explains source structure, value and reference types, variables, mappings, arrays, functions, constructors, modifiers, events, custom errors, inheritance, interfaces, libraries, and the differences among storage, memory, and calldata.

Learning Objectives

By the end of this chapter, a learner should be able to:

  • explain the structure of a Solidity source file;
  • select suitable value and reference data types;
  • distinguish state, local, immutable, and constant variables;
  • implement mappings, arrays, functions, constructors, and modifiers;
  • use events and custom errors appropriately;
  • apply inheritance, interfaces, and libraries;
  • choose correctly among storage, memory, and calldata;
  • identify common design, security, gas, and governance concerns.

1. Solidity Source Structure

A source file commonly begins with a compiler-version directive, followed by imports, declarations, interfaces, libraries, and contracts.

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

contract Counter {
    uint256 private count;

    function increment() external {
        count += 1;
    }

    function getCount() external view returns (uint256) {
        return count;
    }
}

The SPDX identifier communicates licensing information. The pragma directive restricts compilation to compatible compiler versions; it does not upgrade a deployed contract. Solidity 0.8 and later generally include checked arithmetic, meaning addition, subtraction, multiplication, and exponentiation revert on overflow or underflow unless an unchecked block is used.

A contract contains state variables, functions, modifiers, events, errors, constructors, type declarations, and inherited members. Source organization should make the public interface and security boundaries easy to inspect.

2. Data Types

Solidity types are divided broadly into value types and reference types.

2.1 Value types

Value types are copied when assigned or passed to functions. Important examples include:

TypePurpose
booltrue or false values
uint8–uint256Unsigned integers
int8–int256Signed integers
address20-byte account or contract address
address payableAddress that can receive Ether through transfer or send
bytes1–bytes32Fixed-size byte sequences
enumUser-defined set of named options
functionFunction references

Integer widths are multiples of eight bits. For example, uint256 can represent values from 0 through 2²⁵⁶ − 1. Smaller integers may reduce storage when packed with compatible variables, but they do not necessarily reduce arithmetic or memory costs.

The address type should be used for accounts and contracts. A contract should not assume that an address represents a human user: externally owned accounts and contracts can both call functions.

2.2 Reference types

Reference types may refer to data rather than copy it immediately. They include arrays, structs, mappings, and strings. Their data location must usually be specified when used in functions or local variables.

struct Product {
    uint256 id;
    string name;
    address seller;
    bool active;
}

string is a dynamically sized UTF-8 byte sequence. For arbitrary binary data, bytes is generally more appropriate. Fixed-size bytes32 is often cheaper and easier to compare, but it cannot hold arbitrary-length content.

3. Variables and Data Locations

3.1 State variables

State variables are stored permanently in contract storage and therefore affect the blockchain state and gas cost.

uint256 public totalSupply;
address private administrator;

A public state variable automatically generates a getter. private prevents direct access from Solidity contracts, but it does not make the value secret: blockchain storage can generally be inspected externally.

constant variables are compile-time constants and cannot be changed. immutable variables are assigned once, normally in the constructor, and cannot be changed after deployment.

uint256 public constant FEE_BPS = 250;
address public immutable treasury;

If a fee is expressed in basis points, the percentage is calculated as:

percentage = basis points ÷ 100

Thus, 250 basis points equals 2.5 percent. The calculation assumes that the denominator used by the application is 10,000 basis points per 100 percent.

3.2 Storage, memory, and calldata

Solidity provides three important data locations:

LocationLifetime and useTypical implication
storagePersistent contract stateExpensive to modify; survives transactions
memoryTemporary during one callMutable; disappears after execution
calldataRead-only transaction inputOften efficient for external functions

A storage reference can modify persistent state, so it must be used deliberately.

uint256[] private values;

function updateFirst(uint256 newValue) external {
    uint256[] storage localValues = values;
    localValues[0] = newValue;
}

By contrast, a memory copy does not alter the state array:

function copyValues() external view returns (uint256[] memory) {
    uint256[] memory temporary = values;
    return temporary;
}

For external functions receiving arrays or strings, calldata is normally appropriate when the function only reads the arguments.

function sum(uint256[] calldata numbers)
    external
    pure
    returns (uint256 total)
{
    for (uint256 i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
}

A common error is assuming that a storage reference is a harmless alias. It is not: changing it changes the contract’s persistent state.

4. Mappings, Arrays, and Structs

4.1 Mappings

A mapping associates keys with values:

mapping(address => uint256) public balances;
mapping(address => mapping(address => bool)) public approved;

Mappings do not store a list of keys and cannot be enumerated directly. Every possible key has a default value, such as zero for uint256 and false for bool. Consequently, a mapping alone cannot distinguish between “never inserted” and “inserted with the default value.” An additional boolean or an indexable key array is needed when that distinction matters.

Mappings are suitable for direct lookup, such as balances, permissions, or ownership records. They are not suitable when the application must iterate over all entries.

4.2 Arrays

Arrays may be fixed-size or dynamic.

uint256[10] public fixedScores;
uint256[] public scores;

function addScore(uint256 score) external {
    scores.push(score);
}

Dynamic arrays support push and pop. Deleting an element does not automatically preserve ordering. A gas-efficient removal technique is “swap and pop,” but it changes the order of elements:

function removeAt(uint256 index) external {
    require(index < scores.length, "out of bounds");
    scores[index] = scores[scores.length - 1];
    scores.pop();
}

The function must document the ordering consequence. If ordering is semantically important, a different approach is required.

4.3 Structs

Structs group related values:

struct Proposal {
    address proposer;
    uint256 votes;
    bool executed;
}

Proposal[] public proposals;

Structs improve conceptual organization but may increase storage usage. Variables should be ordered thoughtfully because compatible small value types may be packed into one storage slot. Packing is an optimization, not a substitute for profiling and testing.

5. Functions and Visibility

A function declaration includes its name, parameters, visibility, mutability, and return values.

function balanceOf(address account)
    external
    view
    returns (uint256)
{
    return balances[account];
}

Visibility options are:

  • external: callable from outside; external calldata is generally efficient;
  • public: callable internally and externally;
  • internal: callable only within the contract and derived contracts;
  • private: callable only within the defining contract.

Mutability options are:

  • view: must not modify state;
  • pure: must not read or modify state;
  • payable functions: may receive Ether;
  • no mutability keyword: may modify state but do not accept Ether by default.

Function visibility is not merely an organizational choice. An externally callable state-changing function is part of the contract’s attack surface and must be designed as a public protocol operation.

Returning a value does not automatically make it private. Return data and event logs may be visible to users and indexing systems.

6. Constructors and Modifiers

A constructor runs once during deployment. It is useful for establishing administrator addresses, initial parameters, and immutable variables.

contract Vault {
    address public immutable owner;

    constructor(address initialOwner) {
        require(initialOwner != address(0), "zero owner");
        owner = initialOwner;
    }
}

A modifier defines reusable preconditions or postconditions:

modifier onlyOwner() {
    require(msg.sender == owner, "not owner");
    _;
}

function withdraw(uint256 amount) external onlyOwner {
    // protected operation
}

The underscore represents the function body. Modifiers should remain simple and predictable. A modifier that performs complex external calls can obscure control flow and create security risks.

msg.sender is the immediate caller. tx.origin is the original transaction initiator and should not be used for authorization because intermediary contracts can exploit origin-based assumptions.

7. Events and Errors

Events create logs that off-chain applications can index and monitor.

event Deposited(
    address indexed account,
    uint256 amount
);

emit Deposited(msg.sender, msg.value);

Indexed parameters support efficient filtering, subject to the event indexing model. Events are not contract storage and cannot be used by Solidity code as authoritative state.

Custom errors provide structured, gas-efficient failure information:

error InsufficientBalance(uint256 requested, uint256 available);

if (amount > balances[msg.sender]) {
    revert InsufficientBalance(amount, balances[msg.sender]);
}

require remains useful for straightforward conditions and legacy interfaces. Custom errors are preferable when callers or debugging tools benefit from named failure data. Neither events nor errors should expose information that the application treats as confidential.

8. Inheritance, Interfaces, and Libraries

8.1 Inheritance

Inheritance promotes reuse and supports abstract contracts.

abstract contract Ownable {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }
}

contract Treasury is Ownable {
    function setOwner(address newOwner) external onlyOwner {
        owner = newOwner;
    }
}

Solidity uses a linearized method-resolution order for multiple inheritance. Developers must understand which overridden function is selected and must explicitly use override where required. Inheritance should express a genuine “is-a” relationship, not merely be used to obtain convenient code.

8.2 Interfaces

An interface declares externally callable functions without their implementation.

interface IERC20Like {
    function transfer(address to, uint256 amount)
        external
        returns (bool);

    function balanceOf(address account)
        external
        view
        returns (uint256);
}

Interfaces support interoperability and abstraction. They do not guarantee that a target address behaves honestly or conforms perfectly to the declared semantics. External calls can fail, consume gas, return unexpected data, or trigger reentrancy.

8.3 Libraries

Libraries contain reusable functions. A library function may be attached to a type with using for.

library MathTools {
    function double(uint256 value)
        internal
        pure
        returns (uint256)
    {
        return value * 2;
    }
}

contract Example {
    using MathTools for uint256;

    function result(uint256 x) external pure returns (uint256) {
        return x.double();
    }
}

Libraries are appropriate for stateless operations and shared validation. They should not conceal sensitive state changes or make security-critical behavior difficult to audit.

9. A Complete Applied Example

The following illustrative contract models a simple Ether vault. It demonstrates state, a constructor, a modifier, custom errors, events, mappings, and the checks-effects-interactions pattern.

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

contract SimpleVault {
    mapping(address => uint256) public deposited;
    address public immutable administrator;

    error ZeroAmount();
    error InsufficientDeposit(uint256 requested, uint256 available);
    error TransferFailed();

    event Deposit(address indexed account, uint256 amount);
    event Withdrawal(address indexed account, uint256 amount);

    modifier nonZero(uint256 amount) {
        if (amount == 0) revert ZeroAmount();
        _;
    }

    constructor() {
        administrator = msg.sender;
    }

    function deposit() external payable nonZero(msg.value) {
        deposited[msg.sender] += msg.value;
        emit Deposit(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external nonZero(amount) {
        uint256 available = deposited[msg.sender];
        if (amount > available) {
            revert InsufficientDeposit(amount, available);
        }

        deposited[msg.sender] = available - amount;

        (bool success, ) = payable(msg.sender).call{value: amount}("");
        if (!success) revert TransferFailed();

        emit Withdrawal(msg.sender, amount);
    }
}

The withdrawal function first checks the request, then updates internal accounting, and only then performs the external call. This ordering reduces the risk of reentrancy because a reentrant call observes the reduced balance. In production, developers should also consider a well-tested reentrancy guard, emergency controls, withdrawal policies, invariant testing, and the behavior of forced Ether transfers.

Case study: an illustrative community grant contract

Background. A community organization wants members to vote on grants. Each proposal has a description, a recipient, a deadline, and a vote count.

Problem. The organization needs transparent proposal records, protection against unauthorized execution, and an off-chain interface that can discover new proposals efficiently.

Analysis. A struct is appropriate for proposal records, while a mapping can record whether an address has voted on a particular proposal. An array permits proposal identifiers to be enumerated. Events allow a web application to display new proposals without repeatedly scanning every state variable.

Approach. The contract should validate nonzero recipients, enforce a voting deadline, prevent double voting, and separate voting from execution. A modifier such as onlyAdministrator may protect administrative operations. A custom error can report an invalid proposal identifier. The proposal array should not be allowed to grow without considering gas and user-interface consequences.

Outcome. The design provides a transparent state model and an auditable event stream. It does not, by itself, prove that voters are unique humans, that the voting process is legally binding, or that the recipient will use funds appropriately.

Learning points. Data structures should follow access patterns; events supplement rather than replace state; authorization and business rules must be explicit; and governance contracts require social as well as technical design.

10. Security, Governance, and Professional Practice

Solidity code is public infrastructure. Professional development should include unit tests, integration tests, fuzz testing, static analysis, compiler warnings, code review, and, for high-value systems, independent audit. Tests should cover both successful and reverting paths.

Important risks include:

  • reentrancy through external calls;
  • incorrect authorization or use of tx.origin;
  • denial of service caused by unbounded loops;
  • arithmetic, precision, and unit-conversion errors;
  • unsafe assumptions about token standards;
  • reliance on block timestamps or block variables for strong randomness;
  • upgradeability risks and storage-layout incompatibility;
  • loss or misuse of privileged keys;
  • front-running and transaction-order dependence.

Gas efficiency matters because users pay for computation and state changes. However, optimizing before establishing correctness can introduce subtle bugs. A useful priority order is correctness, security, clear invariants, testability, and then measured optimization.

Governance must define who can change parameters, pause activity, upgrade code, or recover funds. Privileged roles should be documented and, where appropriate, controlled by multisignature wallets, timelocks, or transparent community procedures. Legal and regulatory duties may apply to tokens, custody, voting, and financial activity; technical deployment does not remove those obligations.

11. Contemporary Developments

Modern Solidity practice increasingly uses custom errors, compiler checks, formal verification tools, fuzzing, standardized interfaces, and account-abstraction-compatible designs. Layer-2 networks make execution cheaper in many cases but do not eliminate the need for secure code. Contracts may also interact with oracles, cross-chain messaging systems, and off-chain automation, each of which introduces trust and failure assumptions.

AI-assisted coding can help generate tests, documentation, and preliminary code review suggestions. It must not be treated as an auditor or authoritative source. Generated code requires compilation, adversarial testing, manual review, and verification against the intended specification.

Best-Practice Checklist

  1. Pin and test the compiler version used for deployment.
  2. State assumptions about callers, tokens, oracles, and privileged roles.
  3. Use explicit visibility and mutability declarations.
  4. Prefer custom errors for structured failures.
  5. Emit events for important state transitions.
  6. Avoid unbounded loops over user-controlled collections.
  7. Follow checks-effects-interactions and assess reentrancy.
  8. Use calldata for read-only external reference parameters where suitable.
  9. Distinguish storage references from memory copies.
  10. Test edge cases, failed calls, zero values, and boundary conditions.

Chapter Summary

Solidity contracts combine persistent state with publicly callable functions. Value types are copied, whereas reference types require careful data-location choices. State variables use storage; temporary values use memory; and external input is commonly read from calldata. Mappings provide direct lookup but no enumeration, while arrays and structs support ordered and grouped data.

Constructors establish deployment-time state, modifiers reuse preconditions, events expose state transitions to off-chain systems, and custom errors provide structured failure information. Inheritance, interfaces, and libraries support reuse and interoperability but must be used with a clear understanding of control flow and external-call risks. Secure Solidity development depends on explicit authorization, disciplined interaction ordering, testing, auditability, and governance design.

Key Terms

Solidity: A statically typed language for blockchain smart contracts.

Contract: A deployed program containing code and persistent state.

State variable: A variable stored persistently on the blockchain.

Value type: A type whose assignment generally copies its value.

Reference type: A type that refers to data such as an array or struct.

Mapping: A key-to-value lookup structure without direct enumeration.

Array: An ordered collection that may be fixed-size or dynamic.

Struct: A user-defined group of named fields.

Storage: Persistent contract data location.

Memory: Temporary, mutable data location during execution.

Calldata: Read-only input data supplied to an external call.

Modifier: Reusable function precondition or postcondition logic.

Constructor: Code executed once during deployment.

Event: A blockchain log intended for off-chain consumption.

Custom error: A named, structured revert condition.

Inheritance: Reuse and extension of contract definitions.

Interface: A declaration of externally callable functions.

Library: Reusable code intended primarily for shared operations.

Reentrancy: A repeated entry into a contract before an earlier call completes.

Gas: The execution resource charged for blockchain operations.

Review Questions

Short Answer Questions

  1. What is the difference between a state variable and a local variable?
  2. Why can a mapping not be directly enumerated?
  3. Distinguish view from pure functions.
  4. What purpose does an event serve?
  5. Why is tx.origin unsuitable for authorization?

Long Answer Questions

  1. Explain storage, memory, and calldata, including an example of when each is appropriate.
  2. Compare mappings, arrays, and structs for implementing an account registry.
  3. Explain constructors and modifiers and discuss their security implications.
  4. Describe how interfaces and libraries support modular contract development.
  5. Explain the checks-effects-interactions pattern and its relationship to reentrancy.

Analytical / Application Questions

  1. Design the data structures and functions for a contract that records one vote per address per proposal. Justify each choice.
  2. A contract removes an array element by replacing it with the final element. Analyze the gas benefit and the effect on ordering.
  3. Review a withdrawal function that sends Ether before reducing the caller’s balance. Identify the vulnerability and propose a corrected sequence.

Multiple Choice Questions with Answers

Question 1

Which data location provides persistent contract state?

A. memory

B. calldata

C. storage

D. stack

Correct Answer: C. storage

Question 2

What is a principal limitation of a Solidity mapping?

A. It cannot store address keys

B. It cannot be directly enumerated

C. It cannot store zero values

D. It can only contain Boolean values

Correct Answer: B. It cannot be directly enumerated

Question 3

Which function mutability allows reading state but prohibits state modification?

A. pure

B. view

C. payable

D. internal

Correct Answer: B. view

Question 4

When does a constructor normally execute?

A. Every time a function is called

B. Once during contract deployment

C. Only after a contract is destroyed

D. Whenever an event is emitted

Correct Answer: B. Once during contract deployment

Question 5

What is the main purpose of an event?

A. To create persistent storage variables

B. To provide indexed logs for off-chain consumers

C. To prevent all external calls

D. To replace function return values

Correct Answer: B. To provide indexed logs for off-chain consumers

Question 6

Which statement about custom errors is correct?

A. They are structured revert conditions

B. They automatically authorize callers

C. They can only be used in interfaces

D. They permanently store error data

Correct Answer: A. They are structured revert conditions

Question 7

Why is reducing a user balance before an external Ether call safer?

A. It guarantees the call will succeed

B. It follows checks-effects-interactions and limits reentrancy impact

C. It removes all gas costs

D. It makes the function pure

Correct Answer: B. It follows checks-effects-interactions and limits reentrancy impact

Question 8

Which type is generally appropriate for read-only dynamic input to an external function?

A. storage

B. calldata

C. immutable

D. constant

Correct Answer: B. calldata

Question 9

What does an interface primarily provide?

A. A declaration of callable functions without implementation

B. Automatic encryption of state

C. A replacement for the compiler

D. A dynamic array of contracts

Correct Answer: A. A declaration of callable functions without implementation

Question 10

What is a key consequence of using swap-and-pop to remove an array element?

A. The array becomes fixed-size

B. The removed element is always preserved

C. Element ordering may change

D. The operation becomes read-only

Correct Answer: C. Element ordering may change

Smart Contracts
Chapter 5 of 14