Ethereum Tutorial: Learn Smart Contracts from Scratch (2026)
Ethereum took the blockchain idea and made it programmable. Where Bitcoin says 'this is who owns what,' Ethereum says 'this is what happens when conditions are met.' After deploying contracts on mainnet, testnets, and a few layer-2s, I have found the mental model shift from 'app' to 'autonomous agent' to be the hardest part for newcomers.
This tutorial walks through writing, deploying, and testing smart contracts in Solidity. You will learn the EVM execution model, gas optimization, and the security patterns that separate production code from hacks.
Smart Contracts as Autonomous Agents
A smart contract is code deployed to the Ethereum blockchain that runs exactly as written, on every full node, with no possibility of downtime or third-party interference. Once deployed, the contract has its own address, balance, and storage. No one — not even the original author — can modify it (unless upgradeability patterns are baked in).
The Solidity compiler turns human-readable code into EVM bytecode. When a user sends a transaction to the contract's address, every node executes the bytecode and updates their local state. This global re-execution is what guarantees deterministic results — but it also makes every computation expensive.
pragma solidity ^0.8.20;
contract Counter {
uint256 public count;
event Incremented(uint256 newCount);
function increment() external {
count += 1;
emit Incremented(count);
}
}
Gas: The Fuel of the EVM
Every EVM operation costs gas — from a simple addition (3 gas) to writing a new storage slot (20,000 gas). Users pay gas in wei (the smallest ETH unit) and set a gas price they are willing to pay per unit. Miners (now validators post-merge) include transactions with the highest gas prices first.
Gas limits prevent infinite loops: if a transaction runs out of gas, execution halts and all state changes revert, but the fees are still collected. This 'fail-stop' model is your safety net. The practical takeaway: keep storage writes to a minimum, use events instead of storage for logs, and prefer uint256 (the EVM's native word size) over smaller types.
contract GasWatcher {
uint256[] public data;
function append(uint256 value) external {
data.push(value);
}
function appendBatch(uint256[] calldata values) external {
for (uint256 i = 0; i < values.length; i++) {
data.push(values[i]);
}
}
}
ERC-20: The Token Standard
ERC-20 is the standard interface for fungible tokens on Ethereum. Any contract implementing the six required functions can be traded by any wallet or exchange that speaks ERC-20.
The critical vulnerability to watch is the 'approve/transferFrom race condition.' A malicious spender can front-run an approve transaction and spend more than the owner intended. The OpenZeppelin SafeERC20 library mitigates this by using increaseAllowance and decreaseAllowance instead of direct approve calls.
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(string memory name, string memory symbol, uint256 initialSupply)
ERC20(name, symbol)
{
_mint(msg.sender, initialSupply * 10 ** decimals());
}
}
Deploying with Hardhat
Hardhat is the de facto development environment for Ethereum. It compiles Solidity, runs a local network with JavaScript console access, and supports automated testing with ethers.js. The key workflow: write a deployment script, compile with npx hardhat compile, run a local node with npx hardhat node, and deploy with npx hardhat run scripts/deploy.ts --network localhost.
Always verify your contracts on Etherscan after deployment. Hardhat's hardhat-etherscan plugin automates this: npx hardhat verify --network mainnet DEPLOYED_ADDRESS.
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
const config: HardhatUserConfig = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY!]
}
}
};
export default config;
Security: Reentrancy and Beyond
Reentrancy is the exploit that drained $60M from The DAO in 2016. It occurs when a contract calls an external address before updating its own state, allowing the callee to call back into the original function before the first invocation finishes. The fix is simple: update your state before making external calls (checks-effects-interactions pattern).
Other common vulnerabilities include integer overflow (largely solved by Solidity 0.8+'s built-in checks), flash loan attacks on price oracles, and tx.origin misuse. Use the OpenZeppelin ReentrancyGuard modifier as an extra safety net.
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SecureVault is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0;
(bool sent, ) = msg.sender.call{value: amount}("");
require(sent, "Transfer failed");
}
}
Layer-2: Scaling Without Sacrificing Trust
Ethereum mainnet processes ~15 transactions per second. Layer-2 solutions like Arbitrum, Optimism, and zkSync move computation off-chain while posting condensed proofs or data back to L1. Optimistic rollups assume transactions are valid unless challenged (7-day fraud-proof window), while ZK-rollups generate cryptographic validity proofs that are instantly verifiable.
For developers, deploying to L2 is almost identical to L1 — the EVM is bytecode-compatible on most rollups. The main difference is lower gas fees (often cents instead of dollars) and faster confirmation times (~seconds per block).
networks: {
'arbitrum-sepolia': {
url: 'https://sepolia-rollup.arbitrum.io/rpc',
accounts: [process.env.PRIVATE_KEY!],
gasPrice: 100000000,
},
}
Frequently Asked Questions
How is Solidity different from JavaScript or Python?
Solidity is statically typed, compiled (to EVM bytecode), and designed for a gas-metered execution environment. Unlike JS, there is no dynamic dispatch, no garbage collection, and recursion is dangerous. Unlike Python, errors cause full state revert and consumed gas.
Can I upgrade a deployed contract?
Not directly. Upgradeability requires a proxy pattern: a proxy contract stores the state, and a logic contract holds the code. Users interact with the proxy, which delegates calls to the logic contract. The proxy's logic address can be changed by an admin to upgrade behavior.
What is the difference between tx.origin and msg.sender?
msg.sender is the immediate caller of the function (could be a contract). tx.origin is the original externally owned account that initiated the transaction. Never use tx.origin for authentication — it makes your contract vulnerable to phishing attacks via intermediate contracts.
Why do I need to pay gas even for failed transactions?
Every node on the network must execute your transaction to determine its outcome. If execution fails partway, the node has still done the work. The gas spent is compensation for that computational work, regardless of the final outcome. This prevents spam.
Originally published on Ayodhyyya. Last updated June 1, 2026.