Smart Contracts & Solidity

Chapter 3
Solidity Language Fundamentals

Advance Diploma in Blockchain
Smart Contracts & Solidity   •   Advance Diploma

Solidity Language Fundamentals

Introduction

Solidity is a statically typed, contract-oriented programming language used primarily to write programs for Ethereum and Ethereum-compatible blockchains. A Solidity contract is not merely a software component running on a private server: it is deployed to a replicated execution environment in which every validating node must obtain the same result. This requirement strongly influences the language. Code must be deterministic, state changes must be explicit, and computational and storage costs matter because execution consumes gas.

This chapter develops the foundations required to read, write, compile, and reason about Solidity contracts. It covers compiler pragmas, source-file structure, variables, value and reference types, operators, control flow, functions, constructors, and visibility. The examples use modern Solidity conventions, but exact behavior may vary between compiler releases. Production projects should pin and test a specific compiler version rather than relying on an unbounded version range.

Learning Objectives

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

  1. Explain the structure of a Solidity source file and the purpose of a compiler pragma.
  2. Distinguish state, local, and global variables.
  3. Select appropriate value and reference types.
  4. Apply operators and control-flow statements safely.
  5. Define functions with suitable parameters, return values, mutability, and visibility.
  6. Explain constructor execution and contract initialization.
  7. Identify common security, gas, and maintainability risks in basic Solidity code.
  8. Create and assess a small contract using professional development practices.

1. The Solidity Execution Model

A contract contains persistent state and executable functions. When deployed, the contract's initialization code runs once; the resulting runtime code is stored at a blockchain address. Later transactions or calls invoke functions in that runtime code.

Two distinctions are essential:

  • A transaction is submitted to the blockchain, consumes gas, and may change state.
  • A call is normally a read-only interaction performed locally or by a node. A function marked view or pure cannot intentionally modify blockchain state.

All nodes must reproduce the same state transition. Therefore, ordinary Solidity code should not depend on local time, operating-system files, random values supplied by a computer, or network responses. Blockchain-provided information such as block.timestamp is available, but it is not perfectly precise or suitable for strong randomness.

Gas is the execution resource used by the Ethereum Virtual Machine (EVM). Storage writes are generally more expensive than operations on memory or the stack. A sound design therefore stores only information that must persist and avoids unnecessary loops and repeated storage access.

2. Source-File Structure and Compiler Pragmas

A Solidity source file commonly contains, in this order:

  1. An SPDX license identifier.
  2. A compiler version pragma.
  3. Optional imports.
  4. Interfaces, libraries, custom errors, structs, enums, and contracts.

Example:

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

contract Counter {
    uint256 private count;

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

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

The SPDX comment communicates licensing information to tools and users. It does not enforce licensing on-chain.

The statement pragma solidity ^0.8.24; directs the compiler to accept compatible compiler versions beginning at 0.8.24 but below 0.9.0, according to semantic-versioning conventions. A pragma does not install a compiler and does not guarantee that a project actually uses the intended version. Build systems should specify the compiler explicitly and reproduce builds from locked dependencies.

Other forms include:

pragma solidity >=0.8.20 <0.9.0;

and an exact version:

pragma solidity 0.8.24;

A broad pragma may improve library compatibility, but it can permit compilation with versions that have different optimizations or behavior. An exact or narrowly bounded version is usually preferable for audited deployments.

Imports make declarations from another file available. Modern projects should understand whether an import is local, package-based, or remapped by a tool such as Foundry or Hardhat. Dependencies must be reviewed because imported code becomes part of the trust and audit surface.

3. Contracts, State, and Variables

A contract is declared with the contract keyword:

contract Registry {
    address public administrator;
    uint256 public itemCount;

    constructor() {
        administrator = msg.sender;
    }
}

A state variable is declared inside a contract but outside a function. Its value persists in contract storage between transactions. State variables may be declared public, internal, or private; these modifiers control source-level access, not secrecy. A private value can still be observed indirectly through blockchain data and client tools.

A local variable is declared inside a function or block and exists only during execution. A function parameter receives input from a caller. A return variable communicates output to the caller.

Solidity also provides global variables and functions, including:

  • msg.sender: immediate caller of the current call frame.
  • msg.value: amount of native currency sent with the call, denominated in wei.
  • block.timestamp: timestamp associated with the current block.
  • block.number: current block number.
  • address(this): address of the current contract.
  • type(T).min and type(T).max: bounds for some types.

Global blockchain values must be interpreted carefully. For example, msg.sender can be another contract, not necessarily a human account. Authentication based solely on assumptions about externally owned accounts is unsafe.

Naming and declaration practice

Use descriptive names and explicit visibility. State variables are commonly written in lower camel case, constants in upper snake case, and immutable variables in a style that distinguishes them from mutable state. Declare variables near the smallest scope in which they are needed. This reduces accidental reuse and makes auditing easier.

4. Solidity Types

Solidity is statically typed: each expression has a compile-time type. The main distinction is between value types and reference types.

4.1 Value types

Value types are copied when assigned or passed. Important value types include:

TypePurposeExample
boolBoolean truth valuetrue
uintNUnsigned integer of N bitsuint256(100)
intNSigned integer of N bitsint128(-4)
address20-byte blockchain addressaddress(0)
address payableAddress able to receive native currency through Solidity transfer operationspayable(user)
bytes1–bytes32Fixed-size byte sequencebytes32(0)
enumUser-defined finite set of named valuesStatus.Active
Function typeFunction referenceinternal or external function

uint is an alias for uint256; int is an alias for int256. Smaller integer types can reduce storage when packed with compatible variables, but they do not automatically make arithmetic cheaper in every context. Choose a type for correctness first, then optimize after measurement.

Unsigned integers cannot represent negative values. In Solidity 0.8 and later, ordinary arithmetic that overflows or underflows reverts by default. unchecked can disable these checks in a carefully justified block, but it should not be used merely to avoid a revert.

An address is 20 bytes. The zero address, address(0), is commonly used as a sentinel but should not normally be accepted as an owner, recipient, or token destination without a specific reason.

4.2 Reference types

Reference types do not necessarily copy their complete data when assigned. They include:

  • Arrays, such as uint256[] or address[10].
  • Structs, which group related fields.
  • Mappings, which associate keys with values.
  • bytes and string, which are dynamically sized byte sequences or text representations.

Reference types require a data-location annotation in many contexts:

  • storage: persistent contract state.
  • memory: temporary data during a call.
  • calldata: read-only call input, especially efficient for external functions.

Example:

struct User {
    address account;
    uint256 score;
}

User[] private users;
mapping(address => uint256) private scores;

function addUser(address account, uint256 score) external {
    users.push(User(account, score));
}

function inspect(User calldata input)
    external
    pure
    returns (address, uint256)
{
    return (input.account, input.score);
}

A storage reference can modify persistent state; a memory or calldata value cannot directly persist after the function ends. Assigning a storage struct or array to another storage reference creates an alias, so changes through either reference affect the same state. In contrast, assigning to memory generally creates a separate temporary representation.

Mappings are hash tables conceptually, but they do not provide an enumerable list of keys. A mapping value returns its default value for a key that has never been written. Consequently, a separate array or counter is needed when the application must enumerate entries.

Dynamic arrays support push and pop in storage. Deleting an array element may leave a gap; deleting a mapping entry resets its value to its type's default, but it does not expose whether the key was ever written unless the contract stores that information separately.

4.3 Constants and immutable variables

A constant value is fixed at compile time. An immutable value is assigned during contract creation and cannot change afterwards:

uint256 public constant MAX_USERS = 1000;
address public immutable administrator;

constructor() {
    administrator = msg.sender;
}

These mechanisms communicate invariants and may reduce storage costs compared with ordinary state variables.

5. Operators and Expressions

Solidity supports arithmetic, comparison, logical, bitwise, assignment, and conditional operators.

CategoryOperatorsExample
Arithmetic+ - / % *a + b
Comparison== != < <= > >=amount >= minimum
Logical`! &&
Bitwise`&^ ~ << >>`
Assignment= += -= *= /=total += amount
Conditionalcondition ? x : yok ? 1 : 0

Integer division truncates toward zero. For example, 5 / 2 evaluates to 2 for integer operands. It does not produce a fractional value. Division by zero reverts.

Operator precedence can make expressions difficult to audit. Parentheses should be used when intent is not immediately obvious:

uint256 result = (a + b) * c;

Short-circuit evaluation applies to logical operators. In x && y, y is evaluated only if x is true; in x || y, y is evaluated only if x is false. This can be useful, but relying on subtle evaluation order can reduce readability.

Equality and ordering comparisons must use compatible types. Explicit conversions should be used only when the conversion is safe and semantically justified. Converting a larger integer to a smaller one can truncate information; converting signed to unsigned can produce an unexpected result if the value is negative.

6. Control Flow

Solidity provides if, else, for, while, do while, and return. It also supports break and continue in loops.

function classify(uint256 value) external pure returns (uint256) {
    if (value == 0) {
        return 0;
    } else if (value < 10) {
        return 1;
    }
    return 2;
}

Loops should be designed carefully because each iteration consumes gas. A loop over a user-controlled or unbounded array can make a function impossible to execute once the collection grows sufficiently large. Prefer bounded work, pagination, pull-based processing, or off-chain indexing.

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

This function is acceptable for deliberately bounded input, but not necessarily for an arbitrarily large array supplied by an unrestricted caller. A contract should not assume that a loop is safe merely because it works in a small test.

Assertions and error handling require distinct choices:

  • require(condition, "message") reverts when an external precondition fails.
  • Custom errors provide structured, gas-efficient failure information.
  • revert explicitly aborts execution.
  • assert(condition) is intended for invariants that should never fail; an assertion failure indicates a programming error.

Example:

error NotAdministrator(address caller);
error ZeroAddress();

modifier onlyAdministrator() {
    if (msg.sender != administrator) {
        revert NotAdministrator(msg.sender);
    }
    _;
}

Custom errors are generally preferable to long revert strings in production contracts because they encode failure data efficiently and make failure conditions explicit.

7. Functions, Mutability, and Visibility

A function declaration can specify parameters, return values, visibility, and state mutability:

function transferScore(address recipient, uint256 amount)
    external
    returns (bool success)
{
    // state-changing implementation
    return true;
}

Visibility

VisibilityMeaning
externalCallable from outside the contract; parameters are commonly read from calldata.
publicCallable externally and internally; Solidity generates an external access mechanism.
internalCallable within the contract and derived contracts.
privateCallable only within the declaring contract.

Visibility is an access-control mechanism at the language level, not a confidentiality mechanism. A private function's code is still deployed, and a private state variable is not secret.

Use external for functions intended primarily as an external interface, public when internal calls are also required, internal for reusable derived-contract logic, and private for implementation details that must not be inherited.

Mutability

  • view promises not to modify state.
  • pure promises not to read or modify contract state.
  • No mutability keyword permits state modification.
  • payable permits the function to receive native currency.

A function that receives native currency should normally be explicitly marked payable; otherwise a call carrying value reverts. A view or pure declaration is checked by the compiler, but external calls made by a caller can still have broader consequences depending on the call context.

Return values can be named:

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

Named returns improve documentation but do not eliminate the need for explicit, readable control flow.

Modifiers

Modifiers provide reusable preconditions or postconditions:

modifier onlyAdmin() {
    require(msg.sender == administrator, "not administrator");
    _;
}

function setLimit(uint256 newLimit) external onlyAdmin {
    limit = newLimit;
}

The placeholder _ indicates where the function body executes. Modifiers should remain short and predictable. Multiple modifiers execute in their declared order, which should be documented where order affects behavior.

8. Constructors and Initialization

A constructor runs once during deployment and initializes state. It is not callable after deployment.

contract Treasury {
    address public immutable administrator;
    uint256 public limit;

    constructor(uint256 initialLimit) {
        administrator = msg.sender;
        limit = initialLimit;
    }
}

The constructor's msg.sender is the account or contract performing deployment. In factory patterns, this may be a factory contract rather than the ultimate owner, so initialization must be designed deliberately.

Constructor parameters are encoded in deployment data and are not a substitute for secret storage. A secret supplied during deployment can become observable through transaction input or related data. Contracts should not contain private keys, passwords, or other secrets.

Inheritance introduces initialization concerns. A derived contract can invoke a base constructor, and constructor order follows Solidity's inheritance rules. Complex inheritance should be tested explicitly, particularly when ownership or authorization is initialized in more than one place.

9. Integrated Example: A Controlled Registry

The following illustrative contract combines compiler declaration, state variables, mappings, custom errors, a constructor, modifiers, visibility, calldata, events, and controlled state changes.

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

contract CredentialRegistry {
    address public immutable administrator;
    uint256 public totalCredentials;

    mapping(bytes32 => address) private issuerOf;
    mapping(bytes32 => bool) private active;

    error NotAdministrator(address caller);
    error AlreadyRegistered(bytes32 credentialId);
    error UnknownCredential(bytes32 credentialId);
    error ZeroIssuer();

    event CredentialRegistered(bytes32 indexed credentialId, address indexed issuer);
    event CredentialRevoked(bytes32 indexed credentialId);

    constructor() {
        administrator = msg.sender;
    }

    modifier onlyAdministrator() {
        if (msg.sender != administrator) {
            revert NotAdministrator(msg.sender);
        }
        _;
    }

    function register(bytes32 credentialId, address issuer)
        external
        onlyAdministrator
    {
        if (issuer == address(0)) revert ZeroIssuer();
        if (active[credentialId]) {
            revert AlreadyRegistered(credentialId);
        }

        issuerOf[credentialId] = issuer;
        active[credentialId] = true;
        totalCredentials += 1;

        emit CredentialRegistered(credentialId, issuer);
    }

    function revoke(bytes32 credentialId) external onlyAdministrator {
        if (!active[credentialId]) {
            revert UnknownCredential(credentialId);
        }
        active[credentialId] = false;
        emit CredentialRevoked(credentialId);
    }

    function verify(bytes32 credentialId)
        external
        view
        returns (bool isActive, address issuer)
    {
        return (active[credentialId], issuerOf[credentialId]);
    }
}

This design illustrates several professional decisions. The administrator is immutable, reducing the possibility of accidental reassignment. The mappings are private at the Solidity interface level, but their contents are not confidential on a public blockchain. A separate Boolean records whether a credential is active because a mapping lookup alone cannot distinguish an unset address from an intentionally stored zero address. Events provide an efficient off-chain record of important state transitions.

The contract does not enumerate all credentials. That is intentional: enumeration would require an array and additional consistency management. An indexing service can process events, while the contract exposes direct verification for a known identifier.

10. Case Application: University Certificate Registry

This is an illustrative case, not a report of a specific deployment.

Background

A university issues digital certificates and wants employers to verify whether a certificate identifier is valid and has been revoked. The university must retain administrative control while allowing anyone to verify a known certificate.

Problem

A naive implementation might store full student records on-chain and expose a function that loops through every certificate to search for a match. This creates privacy, cost, and scalability problems.

Analysis

Full names, grades, and personal identifiers are inappropriate for public blockchain storage. Even a private variable would not provide confidentiality. An unbounded search loop would become increasingly expensive and could eventually exceed the block gas limit. The verification requirement is lookup by a known identifier, which is well suited to a mapping.

Approach and solution

The university generates a certificate identifier off-chain, preferably from a carefully designed process that does not expose unnecessary personal data. The contract stores a commitment or identifier, the issuing address, and an active or revoked status. Only an authorized administrator can register or revoke a certificate. Anyone can call a view verification function. Events support institutional indexing and audit trails.

Administrative control should be protected by a multisignature wallet rather than a single externally owned account when the value or reputational risk is significant. Deployment and administrative actions should be tested on a public test network, reviewed, and accompanied by a recovery procedure.

Outcome

The design provides constant-key lookup in the contract's conceptual model, avoids storing sensitive student data, and separates public verification from restricted administration. It does not prove that the off-chain issuance process was honest, nor does it make the identifier itself secret.

Learning points

  1. Data minimization is both a privacy and a cost principle.
  2. Mappings are suitable for direct lookup but do not enumerate keys.
  3. Access control must identify the correct administrative principal.
  4. Events complement state; they should not be treated as the only source of current truth.
  5. A smart contract cannot independently guarantee the truth of off-chain facts.

11. Common Errors, Limitations, and Best Practices

Common errors

  • Omitting visibility, making interfaces unclear and sometimes causing compilation failures.
  • Assuming private means secret.
  • Using tx.origin for authorization. Authorization should generally use msg.sender and a deliberate role model.
  • Performing unbounded loops over storage.
  • Ignoring zero addresses and duplicate identifiers.
  • Using block.timestamp as secure randomness.
  • Sending native currency without making a function payable.
  • Confusing memory, calldata, and storage.
  • Using unchecked arithmetic without proving that overflow is impossible or intentionally tolerated.
  • Treating events as a replacement for state.

Professional development workflow

A reliable workflow includes compiler-version pinning, formatting, static analysis, unit tests, property-based tests, deployment scripts, and independent review. Test both successful and reverting paths. Check authorization boundaries, duplicate operations, zero values, extreme integers, empty arrays, and interactions with contracts rather than assuming every caller is a human account.

Solidity code should be small enough to audit. Separate concerns when possible: authentication, accounting, external integrations, and data storage should not become an unstructured monolith. Emit events for significant state transitions, document invariants, and use custom errors with meaningful parameters.

Governance and regulatory considerations

A deployed contract may be difficult or impossible to modify. Upgradeable designs introduce administrative powers and additional risks, including storage-layout errors and upgrade-key compromise. Whether a blockchain certificate, token, or payment service complies with privacy, consumer-protection, financial, and records laws depends on jurisdiction and application context. Technical immutability does not remove legal obligations. Organizations should obtain appropriate legal and compliance advice before deployment.

Chapter Summary

Solidity is a statically typed, contract-oriented language whose programs execute deterministically on the EVM. A source file normally includes licensing information, a compiler pragma, imports, and contract declarations. State variables persist in storage, while local variables and parameters exist during execution. Value types are copied; reference types use storage, memory, or calldata locations and require careful aliasing analysis.

Operators support arithmetic, comparison, logical, bitwise, and assignment expressions, but integer division truncates and arithmetic boundaries must be respected. Control flow should be bounded because gas limits constrain execution. Functions require deliberate choices about visibility, mutability, parameters, return values, and modifiers. Constructors execute once during deployment and must initialize authority and other invariants safely.

A professional contract minimizes sensitive on-chain data, uses explicit access control, validates inputs, emits useful events, avoids unbounded loops, and is tested against adversarial callers and edge cases. Language-level privacy is not blockchain confidentiality, and a contract cannot independently verify off-chain truth. These principles provide the foundation for secure, maintainable smart-contract development.

Key Terms

Compiler pragma: A source directive expressing the Solidity compiler-version range intended for a file.

Contract: A deployed program containing code and persistent state.

EVM: The Ethereum Virtual Machine, which executes Ethereum-compatible contract bytecode.

State variable: A contract-level variable whose value persists in blockchain storage.

Local variable: A temporary variable declared within a function or block.

Value type: A type whose assignment or parameter passing copies the value.

Reference type: A type such as an array, struct, mapping, or dynamic byte sequence whose data may be referenced.

Storage: Persistent EVM data associated with a contract.

Memory: Temporary data available during execution.

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

Mapping: A key-to-value data structure that does not provide key enumeration.

Visibility: A function or state-variable access category such as external, public, internal, or private.

Mutability: A function property describing whether it is pure, view, payable, or state-changing.

Constructor: A function-like deployment routine executed once when a contract is created.

Modifier: Reusable code that checks conditions or wraps a function body.

Custom error: A structured, gas-efficient revert declaration.

Event: An EVM log emitted to communicate state transitions to off-chain systems.

Gas: The execution resource used to measure and limit EVM computation.

Invariant: A condition that should remain true throughout a contract's operation.

Review Questions

Short Answer Questions

  1. What is the purpose of a Solidity compiler pragma?
  2. How do storage, memory, and calldata differ?
  3. Why does a mapping not provide enumeration of its keys?
  4. What is the difference between view and pure functions?
  5. Why should unbounded loops generally be avoided in smart contracts?

Long Answer Questions

  1. Explain the structure of a Solidity source file and discuss why compiler-version selection matters for deployment and auditing.
  2. Compare value types and reference types, including the practical effects of data locations and storage aliasing.
  3. Explain Solidity function visibility and mutability, and recommend appropriate choices for an external verification interface.
  4. Describe constructor execution, deployment authority, and initialization risks in a contract using inheritance or a factory.
  5. Discuss why private variables do not provide confidentiality on a public blockchain and propose a privacy-preserving design for certificate verification.

Analytical / Application Questions

  1. Design the state variables, functions, and access-control rules for a registry in which authorized issuers can add records and the public can verify them. Justify your type and data-location choices.
  2. A contract loops through an ever-growing array to calculate a total. Analyze the gas and availability risks, and propose two alternative designs.
  3. Review a function that authorizes users with tx.origin, accepts a user-supplied address without validation, and uses unchecked arithmetic. Identify the risks and rewrite the design principles that should replace them.

Multiple Choice Questions with Answers

Question 1

What does the Solidity statement pragma solidity ^0.8.24; primarily specify?

A. The amount of gas the contract may consume

B. The compiler-version compatibility range intended for the source file

C. The contract's deployment address

D. The visibility of all functions in the file

Correct Answer: B. The compiler-version compatibility range intended for the source file

Question 2

Which data location is read-only and commonly efficient for external function input?

A. storage

B. memory

C. calldata

D. stackdata

Correct Answer: C. calldata

Question 3

What happens when integer division evaluates 5 / 2 with integer operands?

A. It returns 2.5

B. It returns 3 by rounding upward

C. It reverts because division must be exact

D. It returns 2 by truncating the fractional part

Correct Answer: D. It returns 2 by truncating the fractional part

Question 4

Which function mutability keyword indicates that a function neither reads nor modifies contract state?

A. pure

B. view

C. payable

D. external

Correct Answer: A. pure

Question 5

Why is an unbounded loop over a growing storage array dangerous?

A. It automatically changes every array element to zero

B. Its gas requirement may eventually prevent the function from executing

C. It makes the compiler ignore the function body

D. It converts the array into a mapping

Correct Answer: B. Its gas requirement may eventually prevent the function from executing

Question 6

When does a Solidity constructor normally execute?

A. Every time a public function is called

B. Once during contract creation

C. Only when the contract receives native currency

D. Whenever a state variable is read

Correct Answer: B. Once during contract creation

Question 7

Which statement about a private state variable is correct?

A. Its value is cryptographically hidden from blockchain observers

B. Only the contract can ever cause its value to change, but the value may still be observable

C. It can be read by every derived contract automatically

D. It is stored only in temporary memory

Correct Answer: B. Only the contract can ever cause its value to change, but the value may still be observable

Question 8

What is a principal limitation of a Solidity mapping?

A. It cannot associate addresses with integers

B. It cannot be declared as a state variable

C. It does not provide a built-in enumerable list of keys

D. It always stores values in calldata

Correct Answer: C. It does not provide a built-in enumerable list of keys

Question 9

Which global variable identifies the immediate caller of the current call frame?

A. block.number

B. msg.value

C. msg.sender

D. address(this)

Correct Answer: C. msg.sender

Question 10

What is a principal advantage of a custom error over a long revert string?

A. It allows a function to bypass all authorization checks

B. It provides structured failure information with efficient encoding

C. It makes private state confidential

D. It prevents all arithmetic overflow

Correct Answer: B. It provides structured failure information with efficient encoding

Smart Contracts & Solidity
Chapter 3 of 15