Education › Blockchain › Stage 2: Ethereum and smart contracts

Building a real contract

Events, modifiers, mappings and require — the patterns every practical contract uses.

Beginner→Intermediate ~34 min read Module 7 of 16

You can now read a simple contract. This lesson turns that into the ability to write a real one, by adding the handful of patterns that appear in almost every contract: knowing who called you (msg.sender), guarding functions with require and modifiers, announcing what happened with events, and storing relationships with mappings. These aren't advanced tricks — they're the everyday vocabulary of Solidity. We'll build up a small ownable, pausable contract that uses all of them, the way real contracts are structured.

After this module you can
  • Use msg.sender and the constructor to establish ownership
  • Guard functions with require and reusable modifiers
  • Emit events and explain why off-chain apps depend on them
  • Combine these into a small, realistically structured contract

Who called me? msg.sender and the constructor

Inside any function, Solidity gives you msg.sender — the address that called this function. This one value is the basis of nearly all access control: 'is the caller allowed to do this?' becomes 'is msg.sender the owner?'. The constructor is a special function that runs exactly once, when the contract is deployed, and never again. The classic use is to record who deployed the contract as its owner: inside the constructor, msg.sender is the deployer. Together these two give a contract a notion of an owner from the very first moment it exists.

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

contract Owned {
    address public owner;

    constructor() {
        owner = msg.sender;   // whoever deploys becomes the owner
    }
}

Guarding functions with require

require is how a contract enforces rules. It checks a condition; if the condition is false, it reverts the whole transaction — every state change is undone, as if it never happened — and returns an error message. This all-or-nothing behaviour is a safety feature: a transaction either completes fully and correctly, or has no effect at all, so a contract can never be left half-updated. You use require to check permissions ('only the owner may call this'), validate inputs ('amount must be positive'), and enforce conditions ('the sale must be open'). It is the single most common line in Solidity.

solidity
function setOwner(address newOwner) public {
    require(msg.sender == owner, "not the owner");   // permission check
    require(newOwner != address(0), "zero address"); // input validation
    owner = newOwner;
}
Note

A revert undoes all state changes in the transaction and refunds the unused gas, but you still pay for the work done up to the revert. That's why checks usually come first in a function — fail early, before doing expensive work, so a doomed call wastes as little gas as possible.

Modifiers: reusable guards

When the same require check appears on many functions — 'only the owner' is the classic — you factor it into a modifier: a reusable named guard you attach to functions. The _; inside a modifier marks where the guarded function's body runs. Writing the check once as a modifier keeps every protected function short and makes the intent obvious at a glance (onlyOwner reads like documentation). Modifiers are how contracts stay readable as access rules multiply.

solidity
address public owner;
bool public paused;

modifier onlyOwner() {
    require(msg.sender == owner, "not the owner");
    _;                     // the guarded function's body runs here
}

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

function pause() public onlyOwner {
    paused = true;         // only the owner, and no body needed beyond this
}

Events: telling the outside world

A contract can't push a notification to a website, and reading a contract's storage directly is limited. So contracts emit events — labelled log entries recorded in the transaction receipt — to announce that something happened. An off-chain app (a website, an indexer, an analytics service) *listens* for these events to update its interface: a token transfer emits a Transfer event, and every wallet and explorer watches for it. Events are cheap to emit and can't be read by other contracts, but they are the primary bridge from on-chain activity to off-chain applications. Marking a parameter indexed lets apps efficiently filter for events involving a particular address.

solidity
event OwnerChanged(address indexed previousOwner, address indexed newOwner);

function setOwner(address newOwner) public onlyOwner {
    address previous = owner;
    owner = newOwner;
    emit OwnerChanged(previous, newOwner);   // apps listening will see this
}
Tip

A good rule: emit an event for every meaningful state change. Your frontend, block explorers, and anyone building on your contract rely on events to know what happened without scanning the whole chain. A contract that changes state silently is hard to build a usable app around.

Putting it together

Here is a small contract using every pattern from this lesson: it has an owner set in the constructor, an onlyOwner modifier, a pausable flag, a mapping of balances, require checks, and events. This is roughly the shape of a real contract's control scaffolding — the same structure underlies tokens, crowdfunds, and marketplaces. Read it and you'll recognise msg.sender, the constructor, modifiers, require, mappings, and events all working together.

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

contract Vault {
    address public owner;
    bool public paused;
    mapping(address => uint256) public balanceOf;

    event Deposited(address indexed who, uint256 amount);

    constructor() { owner = msg.sender; }

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

    function setPaused(bool p) public onlyOwner { paused = p; }

    function deposit() public payable whenNotPaused {
        balanceOf[msg.sender] += msg.value;
        emit Deposited(msg.sender, msg.value);
    }
}

With this scaffolding vocabulary in hand, you're ready for the next lesson: token standards, where these exact patterns are used to build the ERC-20 and ERC-721 contracts that power most of the ecosystem.

Hands-on practice

Build an ownable, pausable contract

  1. Write a constructor that records the deployer as owner, and explain why msg.sender there is the deployer.
  2. Add an onlyOwner modifier and use it to protect a function, then explain what _; does.
  3. Add a require that rejects the zero address as a new owner, and explain what a revert does to state.
  4. Emit an event when the owner changes, and describe which off-chain systems would listen for it.
  5. Combine everything into a small vault with a pausable deposit and confirm each pattern is present.
Cheat sheet

Building a real contract — at a glance

Main things to focus on

  • msg.sender is the caller's address — the basis of access control.
  • The constructor runs once at deployment; use it to set the owner.
  • require checks a condition and reverts the whole transaction if it fails.
  • A revert undoes all state changes; put checks first to fail cheaply.
  • Modifiers factor a repeated require into a reusable guard; _; is where the body runs.
  • Events are logs that off-chain apps listen to; emit one for every meaningful state change.

Identity

msg.senderthe address calling the function
msg.valueether sent with the call
constructor()runs once at deployment
owner = msg.senderrecord the deployer as owner

Guards

require(cond, msg)revert if the condition is false
revertundo all state changes in the tx
modifier onlyOwnera reusable named guard
_;where the guarded body runs

Events

event Name(...)declare a log type
emit Name(...)record it in the receipt
indexedmakes a parameter filterable
off-chain listensfrontends react to events

State

mapping(addr=>uint)per-address balances
bool pauseda flag guarded by a modifier
payablefunction may receive ether
address(0)the zero address; often rejected

Common pitfalls

  • Forgetting that the constructor runs only once — you can't rely on it to re-check things later.
  • Doing expensive work before the require checks, wasting gas when a call is doomed to revert.
  • Leaving out the _; in a modifier, so the guarded function body never runs.
  • Changing state without emitting an event, leaving frontends unable to react.
  • Assuming a revert costs nothing — you still pay for the gas used up to the failure.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →