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.
- 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.
// 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.
function setOwner(address newOwner) public {
require(msg.sender == owner, "not the owner"); // permission check
require(newOwner != address(0), "zero address"); // input validation
owner = newOwner;
}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.
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.
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
}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.
// 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.