Introduction to Smart Contracts
A smart contract is a self-executing program that automates actions according to the terms of an agreement. It consists of code and data residing at a specific address on the Ethereum blockchain. These contracts run exactly as programmed without the possibility of downtime, censorship, fraud, or third-party interference.
Let's start with a basic example that sets a variable value and exposes it for other contracts to access.
Storage Contract Example
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract SimpleStorage {
uint storedData;
function set(uint x) public {
storedData = x;
}
function get() public view returns (uint) {
return storedData;
}
}The first line specifies that the source code is licensed under GPL version 3.0. The next line indicates that the source is written for Solidity version 0.4.16 or newer language versions up to, but not including, version 0.9.0. This ensures the contract cannot be compiled with new compiler versions where it might behave differently.
The uint storedData; line declares a state variable named storedData of type uint (256-bit unsigned integer). Think of it as a slot in a database that you can query and alter by calling functions that manage the database. The contract defines functions set and get that can modify or retrieve the variable's value.
To access a current contract member (like a state variable), you typically don't need to add a this. prefix - you can access it directly by name. This contract allows anyone to store a single number accessible to anyone worldwide, with the number remaining stored in the blockchain history even if overwritten.
Subcurrency Example
The following contract implements a simple cryptocurrency:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
contract Coin {
address public minter;
mapping(address => uint) public balances;
event Sent(address from, address to, uint amount);
constructor() {
minter = msg.sender;
}
function mint(address receiver, uint amount) public {
require(msg.sender == minter);
balances[receiver] += amount;
}
error InsufficientBalance(uint requested, uint available);
function send(address receiver, uint amount) public {
if (amount > balances[msg.sender])
revert InsufficientBalance({
requested: amount,
available: balances[msg.sender]
});
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
}This contract introduces several important concepts:
address public minter;declares a publicly accessible address type state variablemapping(address => uint) public balances;creates a public state variable of a more complex data type that maps addresses to unsigned integersevent Sent(address from, address to, uint amount);declares an event that clients can listen for on the blockchain- The constructor function only runs during contract creation
- Error types allow providing information about why an operation failed
- The
sendfunction includes safety checks to prevent insufficient balance transfers
The public keyword automatically generates getter functions that allow accessing these state variables from outside the contract.
Blockchain Basics
For developers, blockchain concepts aren't difficult to understand because most complex aspects (mining, hashing, cryptography, peer-to-peer networks) simply provide specific functionality and promises. You can use these features without understanding the underlying technology.
Transactions
A blockchain is a globally shared transactional database where anyone can join the network to read database records. To change something in the database, you must create a transaction accepted by all participants. "Transaction" means what you want to do either completes entirely or not at all. When your transaction applies to the database, other transactions cannot modify it simultaneously.
Transactions are always signed by the sender (creator), providing simple access protection mechanisms. In cryptocurrency systems, a simple check ensures only holders of account keys can transfer from those accounts.
Blocks
The main obstacle to overcome is the "double-spend attack": if two transactions both want to empty an account, which one is valid? Only one transaction can be valid, typically the first one accepted. The solution is that a globally accepted transaction order selects which transaction to include.
Transactions bundle into "blocks" that execute and distribute across all participating nodes. If two transactions conflict, the second one eventually rejects and doesn't become part of the block. These blocks form a linear sequence in time - hence "blockchain." Blocks add to the chain at regular intervals (approximately 17 seconds for Ethereum).
As part of the "order selection mechanism" (mining), blocks may sometimes revert, but only at the chain's "tip." The more blocks added after a transaction, the less likely revert becomes.
Ethereum Virtual Machine
Overview
The Ethereum Virtual Machine (EVM) is the runtime environment for Ethereum smart contracts. It's not just sandboxed but completely isolated, meaning code running inside the EVM cannot access the network, file system, or other processes. Even access between smart contracts is limited.
Accounts
Ethereum has two account types sharing the same address space:
- External accounts: Controlled by public-private key pairs (humans)
- Contract accounts: Controlled by code stored with the account
External account addresses derive from public keys, while contract addresses determine during contract creation (from the creator address and transaction count from that address).
Each account has a persistent key-value store mapping 256-bit words to 256-bit words, called storage. Additionally, each account has an ether balance (in "Wei", where 1 ether equals 10¹⁸ wei), which changes when sending ether-containing transactions.
Transactions
Transactions are messages sent from one account to another (possibly the same or special zero account). They can contain binary data ("payload") and ether. If the target account contains code, that code executes with the payload as input parameters.
If the target account isn't set (transaction has no recipient or recipient set to null), the transaction creates a new contract. The contract address isn't the zero address but derives from the sender and their transaction count ("nonce"). The payload of such contract creation transactions considers EVM bytecode and executes. The execution output stores permanently as the contract's code.
Gas
Upon creation, each transaction charges a certain amount of gas, paid by the transaction originator (tx.origin). As the EVM executes the transaction, gas gradually depletes according to specific rules. If gas runs out at any point (becomes negative), it triggers a gas exhaustion exception that ends execution and reverts all state modifications in the current call stack.
This mechanism incentivizes economical use of EVM execution time and compensates EVM executors (miners/stakers). Since each block has a maximum gas amount, it also limits the work required to verify blocks.
The gas price is a value set by the transaction originator, who must prepay gas_price * gas to the EVM executor. If some gas remains after execution, it refunds to the transaction originator. If an exception reverts changes, already used gas doesn't refund.
Storage, Memory, and Stack
The Ethereum Virtual Machine has three data areas: storage, memory, and stack.
Each account has a data area called storage that persists between function calls and transactions. Storage is a key-value store mapping 256-bit words to 256-bit words. Enumerating storage from within a contract isn't possible, and reading is relatively expensive while initializing and modifying storage is even more costly.
The second data area is memory, which gets a freshly cleared instance for each message call. Memory is linear and can address at the byte level, but reads limit to 256-bit width while writes can be 8-bit or 256-bit. When accessing previously untouched memory, memory extends by one word (256 bits), with gas costs increasing as memory grows larger.
The EVM is stack-based rather than register-based, so all computation performs on an area called the stack. The stack has a maximum of 1024 elements, each 256 bits wide. Stack access limits to the top: you can copy one of the top 16 elements to the top or swap the top element with one of the 16 below it. All other operations take the top elements and push the result to the top.
Instruction Set
The EVM instruction set keeps minimal to avoid incorrect implementations that could cause consensus problems. All instructions operate on basic data types, 256-bit words, or memory slices. The set includes common arithmetic, bit, logical, and comparison operations. Conditional and unconditional jumps are possible. Contracts can access relevant current block properties like number and timestamp.
Message Calls
Contracts can call other contracts via message calls or send ether to non-contract accounts. Message calls resemble transactions: they have source, target, data, ether, gas, and return data. In fact, every transaction consists of a top-level message call that can create further message calls.
Contracts can decide how much of their remaining gas to send with internal message calls and how much to retain. If an out-of-gas exception (or any other exception) occurs in an internal call, this indicates by an error value pushed onto the stack. Only the gas sent with the call gets used up. In Solidity, the calling contract causes a manual exception by default, so exceptions "bubble up" the call stack.
Calls limit to 1024 depth, meaning loops should prefer over recursive calls for complex operations. Additionally, only 63/64 of the gas can forward in a message call, making the practical depth limit slightly below 1000.
Delegate Call and Libraries
A special message call variant called delegatecall exists identical to a regular call except that the target address's code executes in the context of the calling contract, with msg.sender and msg.value unchanged.
This means a contract can dynamically load code from different addresses at runtime. Storage, current address, and balance still refer to the calling contract, only the code comes from the called address.
This enables implementing "library" functionality in Solidity: reusable library code that can apply to a contract's storage.
Logs
A special indexed data structure exists whose data maps up to the block level. This feature calls logs, and Solidity uses it to implement events. Contracts cannot access log data after creation, but this data can access efficiently from outside the blockchain. Since part of the log data stores in bloom filters, networks nodes without the entire blockchain (light clients) can efficiently find these logs.
Create
Contracts can create other contracts via a special instruction (not simply calling the zero address). The only difference between create calls and regular message calls is that the payload executes, the execution result stores as contract code, and the caller/creator receives the new contract's address on the stack.
Deactivate and Self-Destruct
The only way to remove code from the blockchain is when a contract at that address performs a selfdestruct operation. The remaining ether stored at that address sends to a designated target, then the storage and code remove from the state.
Warning: From version 0.8.18 onward, using selfdestruct in Solidity and Yul triggers deprecation warnings as the SELFDESTRUCT opcode will undergo significant behavioral changes described in EIP-6049.
Even if a contract removes via selfdestruct, it remains part of blockchain history and likely retains by most Ethereum nodes. Using selfdestruct differs from deleting data from a hard drive.
To deactivate contracts, you can disable them by changing some internal state so that all functions revert when called, making the contract unusable as it immediately returns ether.
Precompiled Contracts
A small range of contract addresses is special. The address range between 1 and 8 (inclusive) contains "precompiled contracts" that can call like other contracts, but their behavior (and gas consumption) isn't defined by EVM code stored at that address (they contain no code) but implemented by the EVM execution environment itself.
Different EVM-compatible chains may use different precompiled contract sets. New precompiled contracts might add to the Ethereum mainchain in the future, but you can reasonably expect they'll always be between 1 and 0xffff (inclusive).
Frequently Asked Questions
What is a smart contract?
A smart contract is a self-executing program stored on a blockchain that automatically executes when predetermined conditions are met. It consists of code and data that resides at a specific address on the blockchain, enabling automated agreements without intermediaries.
How do gas fees work in Ethereum?
Gas fees represent the computational effort required to execute operations on the Ethereum network. Users pay gas fees to compensate miners/validators for the resources needed to process and validate transactions. The total fee equals gas used multiplied by gas price, with unused gas refunded to the sender.
What's the difference between storage and memory in EVM?
Storage is persistent data that remains between function calls and transactions, stored on the blockchain. Memory is temporary data that clears after each external function call. Storage operations are more expensive than memory operations in terms of gas costs.
Can smart contracts be modified after deployment?
No, smart contracts are immutable once deployed to the blockchain. Their code cannot change, though developers can implement upgrade patterns using proxy contracts that delegate to implementation contracts which can be replaced.
What are Ethereum events used for?
Events allow logging specific actions on the blockchain that external applications can detect and respond to. They provide a gas-efficient way to communicate that something happened in a contract, enabling dApp frontends to update their state accordingly.
How do I handle errors in Solidity contracts?
Solidity provides several error handling mechanisms including require() for validating conditions, revert() for canceling execution, and custom error types that provide detailed information about failure reasons. Proper error handling ensures contracts fail predictably and safely.
👉 Explore advanced smart contract strategies
What security considerations are important for smart contracts?
Critical security practices include proper access controls, overflow/underflow protection, reentrancy guards, and comprehensive testing. Contracts should follow the principle of least privilege and implement emergency stops or upgrade mechanisms when appropriate.