Solidity and Smart Contracts – Functions, Inheritance and Blockchain Mechanics, Foundations of CS – Study Notes
offline

Difficulty, Prerequisites and Context

Difficulty: Intermediate

Prerequisites: Part 1 of these study notes (Solidity language fundamentals, data types, mappings, structs). You should be comfortable with what a contract is and how state variables work before tackling functions, inheritance and error handling.

Big picture: Part 1 covered what data a smart contract holds. This part covers what a contract does: how functions are defined and restricted, how contracts reuse code through inheritance, how errors are caught before they waste gas, and how the Ballot voting contract ties everything together into a working system. These concepts are where Solidity diverges most from conventional programming, because every function call has a cost, deployed code is permanent, and bugs can lose real money.


TL;DR

Solidity functions have four visibility levels and two state-access modifiers (pure, view) that affect both security and gas cost. Inheritance uses virtual and override keywords. Error handling relies on require() to revert invalid transactions. Global variables like msg.sender and msg.value identify callers and track ETH, while storage and memory keywords control where data lives during execution.


Key Terms

Function visibility (public, internal, private, external)

Keywords that control who can call a function. public means anyone; internal means only this contract and its children; private means only this contract; external means only callers from outside. Think of them as access levels, from wide open to locked down.

Pure function

A function that neither reads nor writes the contract's state. It operates only on its inputs and is gas-free when called externally. In simple terms, it is a stateless calculator.

View function

A function that reads state but does not modify it. Also gas-free when called externally. Think of it as a read-only query.

Payable

A modifier on a function or address indicating it can receive ETH. Without payable, any attempt to send ETH to that function will revert.

msg.sender

A global variable holding the address of the account (user or contract) that called the current function. In simple terms, it answers the question "who is calling this?"

msg.value

The amount of ETH (in wei) sent along with the current transaction. One ETH equals 10^18 wei.

block.timestamp

The Unix timestamp of the current block, set by the miner/validator. Used for deadline logic, though it can be slightly manipulated by miners.

Constructor

A special function that runs exactly once, at the moment the contract is deployed. Used to set initial state. Think of it as the setup method that fires on first launch.

Event

A log entry emitted during contract execution that off-chain applications (front-ends, block explorers) can listen for. Events are stored in transaction logs, not in contract storage, so they are cheaper than state writes.

Indexed parameter

A parameter in an event declaration marked indexed, which allows off-chain tools to filter logs by that parameter efficiently. Up to three indexed parameters per event.

require()

A built-in function that checks a condition and reverts the transaction (refunding remaining gas) if the condition is false. Think of it as a guard clause that protects against invalid inputs.

virtual

A keyword on a parent function indicating it can be overridden by a child contract.

override

A keyword on a child function indicating it replaces the parent's implementation of a virtual function.

Storage

Persistent data location on the blockchain. State variables live in storage. Every read and write to storage costs gas.

Memory

Temporary data location that exists only during function execution. Cheaper than storage. Function parameters and local variables of reference types (arrays, strings, structs) must be explicitly marked memory or storage.

address(this).balance

Returns the total amount of ETH (in wei) currently held by the contract.


Core Content

Functions and Visibility

  • Functions are declared with the function keyword, can accept parameters and return values.

  • Four visibility levels:

    • public – callable from anywhere (other contracts, external accounts, internally).

    • external – callable only from outside the contract. Slightly more gas-efficient than public for external calls because it reads arguments directly from calldata.

    • internal – callable within the contract and by derived (child) contracts.

    • private – callable only within the declaring contract.

  • Example:

function setValue(uint _value) public {
    value = _value;
}

Pure and View Functions

  • pure: does not read or write state. Used for computation only. Gas-free when called externally (not from within a state-changing transaction).

function add(uint a, uint b) public pure returns (uint) {
    return a + b;
}
  • view: reads state but does not modify it. Also gas-free when called externally.

function getBalance(address _account) public view returns (uint) {
    return balances[_account];
}
  • If you call a view or pure function from within a state-changing function, it does cost gas (because the outer transaction already pays for execution).

msg.sender and Caller Identification

  • msg.sender is a globally available variable that holds the address of whoever called the current function.

  • Common uses: authenticating users, managing permissions, tracking who deposited or withdrew funds.

  • Example (deposit pattern):

function deposit() public payable {
    balances[msg.sender] += msg.value;
}
  • msg.sender changes depending on the call chain. If Contract A calls Contract B, then inside B, msg.sender is A's address, not the original user's.

Constructor Functions

  • A constructor runs exactly once, at deployment time.

  • Used to set the contract owner, initialise arrays or mappings, or accept initial configuration.

  • Cannot be called again after deployment.

  • Example:

constructor(uint[] memory initialData) {
    dataArray = initialData;
}

Events and Indexed Parameters

  • Events write log entries to the transaction receipt. They are cheaper than storage writes and are the standard way for front-ends to react to on-chain activity.

  • Declared with the event keyword and emitted with emit.

  • Up to three parameters can be marked indexed, enabling efficient filtering.

  • Example:

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

function transfer(address _to, uint256 _amount) public {
    // transfer logic
    emit Transfer(msg.sender, _to, _amount);
}

Error Handling with require()

  • require(condition, "error message") checks a condition at runtime.

  • If the condition is false, the transaction reverts and remaining gas is refunded.

  • Used for input validation, access control, and enforcing business rules.

  • Example:

function deposit(uint256 amount) public {
    require(amount > 0, "Deposit must be greater than zero");
    balances[msg.sender] += amount;
}
  • Solidity also offers assert() (for internal invariants, consumes all gas on failure) and revert() (unconditional revert with a message), but require() is the most common for input guards.

Inheritance

  • One contract can inherit from another using the is keyword: contract Child is Parent { }.

  • The parent marks overridable functions with virtual; the child uses override.

  • Multiple inheritance is supported. Solidity uses C3 linearisation to resolve conflicts.

  • Example:

contract Parent {
    function greet() public pure virtual returns (string memory) {
        return "Hello from Parent";
    }
}

contract Child is Parent {
    function greet() public pure override returns (string memory) {
        return "Hello from Child";
    }
}

Storage vs. Memory

  • Storage: persistent, on-chain. State variables default to storage. Reads and writes cost gas (writes cost significantly more).

  • Memory: temporary, exists only during function execution. Function parameters and local reference-type variables must be annotated.

  • Calldata: similar to memory but read-only. Used for external function parameters; cheaper than memory because no copy is made.

  • Rule of thumb: use memory for data you need only during the function call; use storage for data that must persist between calls.

Ballot Contract Workflow

The Ballot contract is a common teaching example that ties together most of the concepts above:

  1. Deployment: the chairperson deploys the contract, passing proposal names to the constructor.

  1. Voter registration: the chairperson calls a function to grant voting rights to specific addresses (uses msg.sender to verify only the chairperson can do this, enforced by require()).

  1. Voting: registered voters call a vote function, selecting a proposal index. The contract checks via require() that the voter has not already voted.

  1. Vote storage: votes are tracked using mappings and/or arrays.

  1. Vote counting: a view function iterates through proposals to find the one with the highest vote count.

  1. Winner declaration: a view function returns the winning proposal's name.

This workflow demonstrates access control (msg.sender + require()), state management (mappings, structs), events, and the constructor pattern.


Real-World Applications

The require() pattern is the foundation of every DeFi protocol's safety checks: Uniswap uses it to verify sufficient liquidity before a swap, and lending protocols use it to prevent withdrawals that would make a loan under-collateralised. Events are how every block explorer (Etherscan, for example) tracks token transfers in real time. The inheritance model is used heavily in the OpenZeppelin library, where developers inherit battle-tested base contracts (ERC-20, ERC-721, Ownable, Pausable) rather than writing security-critical code from scratch.


Common Misconceptions

  • Students often think view and pure functions are always free. They are gas-free only when called externally (e.g. from a front-end read call). When called from within a state-changing transaction, they cost gas like any other computation.

  • Students often confuse msg.sender with tx.origin. msg.sender is the immediate caller (which may be another contract), while tx.origin is always the original external account that started the transaction. Using tx.origin for authorisation is a known security vulnerability.

  • Students sometimes believe a constructor can be called after deployment. It cannot. If you need a re-initialisable pattern, you must write a separate initialise() function with access control.

  • Students often assume require() and assert() are interchangeable. require() is for input validation and refunds remaining gas on failure. assert() is for internal invariants and consumes all remaining gas on failure. Use require() for user-facing checks.


Why It Matters / Exam Flags

  • ⚠️ Know all four function visibility modifiers and be able to pick the correct one for a given scenario (e.g. "this function should only be called from outside the contract" = external).

  • ⚠️ Distinguish pure from view: can the function read state? If yes, it is view. If it reads nothing and writes nothing, it is pure.

  • ⚠️ Expect a question on require(): given a function, identify where a require() guard should go and what condition it should check.

  • ⚠️ Understand the Ballot contract workflow end to end. Be able to trace a transaction from deployment through voter registration, voting, and winner declaration.

  • ⚠️ Be able to explain the difference between storage and memory and when you would use each.

  • ⚠️ Know that virtual goes on the parent and override goes on the child, and that both keywords are required for the override to compile.


Quick Self-Test

  1. True or false: A pure function can read the contract's state variables. (False. pure functions cannot read or write state.)

  1. Fill in the blank: To allow a function to receive ETH, it must be marked ____. (payable)

  1. True or false: require() consumes all remaining gas when it fails. (False. require() refunds remaining gas. assert() is the one that consumes all gas.)

  1. Fill in the blank: In inheritance, the parent function is marked virtual and the child function is marked ____. (override)

  1. True or false: Data stored in memory persists after the function finishes executing. (False. Memory is erased when the function returns.)


Practice Q&A

Q: What is the difference between external and public function visibility?

A: Both can be called from outside the contract. The difference is that public functions can also be called internally (from within the contract), while external functions cannot be called internally without using this.functionName(). external is slightly more gas-efficient for external calls because it reads arguments from calldata rather than copying them to memory.

Q: Explain what happens when require(amount > 0, "Deposit must be greater than zero") evaluates to false.

A: The transaction reverts, all state changes made during that transaction are undone, the error message is returned to the caller, and remaining gas is refunded.

Q: In the Ballot contract, why is msg.sender checked in the voter registration function?

A: To verify that only the chairperson (the address that deployed the contract) is granting voting rights. The constructor stores the deployer's address, and require(msg.sender == chairperson) ensures no other address can call the registration function.

Q: What keywords are needed for a child contract to replace a parent's function?

A: The parent function must be marked virtual, and the child function must be marked override. Both keywords are required; omitting either will cause a compilation error.

Q: When would you mark a function parameter as memory versus storage?

A: Use memory when the function only needs temporary access to the data during execution (the most common case for function parameters). Use storage when the function needs a reference to the actual on-chain data and may modify it persistently. For example, function processArray(uint[] memory tempData) works with a temporary copy, while a storage pointer inside a function body points directly to a state variable.

Q: Why are events preferred over state variables for logging activity?

A: Events write to transaction logs rather than contract storage, making them significantly cheaper in gas. They are designed for off-chain consumption (front-ends, analytics) and are not readable from within other contracts.


Connections to Other Topics

This material builds directly on Part 1 (language fundamentals). The visibility and data-type knowledge from Part 1 feeds into understanding function signatures and modifier stacking here. Inheritance connects to object-oriented programming concepts covered earlier in the Foundations of CS course. The Ballot contract is a synthesis exercise: if you can trace its full lifecycle, you can handle most exam questions on this topic.

Beyond this course, these patterns (access control via msg.sender, guard clauses via require(), event-driven architecture) are the foundation of every production smart contract, from ERC-20 tokens to DeFi lending pools to NFT marketplaces.


Related Terms / Search Tags

Solidity functions, function visibility, public, private, internal, external, pure function, view function, payable, msg.sender, msg.value, block.timestamp, constructor Solidity, events Solidity, indexed parameter, emit, require(), assert(), revert(), error handling Solidity, inheritance Solidity, virtual, override, is keyword, multiple inheritance, C3 linearisation, storage vs memory, calldata, gas cost, Ballot contract, voting smart contract, address(this).balance, wei, ERC-20, OpenZeppelin, dApp, decentralised application, Purdue CS, Foundations of Computer Science