Web3 & Ethereum Basics Cheat Sheet
Introduces core blockchain and Ethereum concepts, a minimal Solidity smart contract, and calling contracts from JavaScript with ethers.js.
Core Web3/Ethereum Concepts
The vocabulary of Ethereum development.
- Blockchain- Append-only distributed ledger; each block cryptographically references the prior one
- Smart contract- Immutable code deployed to the chain that executes deterministically when called
- Wallet- Holds a private key that signs transactions; the address derives from the public key
- Gas- Fee paid in ETH to compensate validators for computation/storage a transaction consumes
- EVM- Ethereum Virtual Machine — executes smart contract bytecode identically on every node
- RPC provider- Service (e.g. Infura, Alchemy, or a self-hosted node) exposing JSON-RPC endpoints
- Mainnet vs Testnet- Testnets (e.g. Sepolia) use worthless test ETH for development before mainnet
Minimal Solidity Contract
A storage contract with an owner-only setter and an event.
// SPDX-License-Identifier: MITpragma solidity ^0.8.20;contract SimpleStorage { uint256 private value; address public owner; event ValueChanged(uint256 newValue); constructor() { owner = msg.sender; // deployer becomes the owner } function setValue(uint256 _value) public { require(msg.sender == owner, "Not authorized"); value = _value; emit ValueChanged(_value); } function getValue() public view returns (uint256) { return value; // 'view' = no gas cost when called off-chain }}
Interacting with a Contract (ethers.js)
Reading state for free, then sending a signed transaction.
import { ethers } from 'ethers';// Read-only provider (no signing capability)const provider = new ethers.JsonRpcProvider('https://sepolia.infura.io/v3/YOUR_KEY');// Signer wraps a private key/wallet for sending transactionsconst wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);const abi = ['function getValue() view returns (uint256)', 'function setValue(uint256)'];const contract = new ethers.Contract('0xContractAddress...', abi, wallet);// Read call — free, no transaction, no gasconst current = await contract.getValue();// Write call — creates a signed transaction, costs gas, needs confirmationconst tx = await contract.setValue(42);const receipt = await tx.wait(); // waits for the tx to be minedconsole.log('Confirmed in block', receipt.blockNumber);
Gas & Transaction Terms
Concepts you need to reason about transaction cost.
- Gas limit- Maximum gas units a transaction may consume before it reverts
- Base fee / gas price- Cost per gas unit in gwei (1 gwei = 10^-9 ETH), set by EIP-1559 dynamics
- Priority fee (tip)- Extra amount paid to incentivize validators to include the transaction sooner
- Nonce- Sequential per-account counter preventing replay and enforcing ordering
- Revert- Failed transaction that undoes state changes, but the sender still pays gas already spent
- Wei/Gwei/Ether- Denominations: 1 ETH = 10^9 Gwei = 10^18 Wei
Checks-Effects-Interactions & Reentrancy Guard
The canonical defense against reentrancy attacks, where an external call re-enters the contract before state is finalized.
// SPDX-License-Identifier: MITpragma solidity ^0.8.20;contract Vault { mapping(address => uint256) public balances; bool private locked; modifier nonReentrant() { require(!locked, "Reentrant call"); locked = true; _; locked = false; } function withdraw(uint256 amount) external nonReentrant { // 1. Checks require(balances[msg.sender] >= amount, "Insufficient balance"); // 2. Effects -- update state BEFORE the external call balances[msg.sender] -= amount; // 3. Interactions -- external call happens last (bool success, ) = msg.sender.call{value: amount}(""); require(success, "Transfer failed"); }}
Minimal ERC-20 Token
The standard fungible token interface: balances, transfers, and delegated allowances.
// SPDX-License-Identifier: MITpragma solidity ^0.8.20;contract SimpleToken { string public name = "SimpleToken"; string public symbol = "SIM"; uint8 public decimals = 18; uint256 public totalSupply; mapping(address => uint256) public balanceOf; mapping(address => mapping(address => uint256)) public allowance; event Transfer(address indexed from, address indexed to, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value); constructor(uint256 initialSupply) { totalSupply = initialSupply; balanceOf[msg.sender] = initialSupply; } function transfer(address to, uint256 value) external returns (bool) { require(balanceOf[msg.sender] >= value, "Insufficient balance"); balanceOf[msg.sender] -= value; balanceOf[to] += value; emit Transfer(msg.sender, to, value); return true; } function approve(address spender, uint256 value) external returns (bool) { allowance[msg.sender][spender] = value; // race condition: use increase/decreaseAllowance in prod emit Approval(msg.sender, spender, value); return true; } function transferFrom(address from, address to, uint256 value) external returns (bool) { require(allowance[from][msg.sender] >= value, "Allowance exceeded"); allowance[from][msg.sender] -= value; balanceOf[from] -= value; balanceOf[to] += value; emit Transfer(from, to, value); return true; }}
Common Smart Contract Vulnerabilities
Attack patterns every Solidity author must defend against before mainnet deployment.
- Reentrancy- External call re-enters the contract before state updates; fix with checks-effects-interactions + guards
- Integer overflow/underflow- Solidity 0.8+ reverts on overflow by default; pre-0.8 code needs SafeMath
- Access control gaps- Missing onlyOwner/role checks on privileged functions (mint, upgrade, withdraw)
- Front-running (MEV)- Miners/searchers reorder or insert transactions seeing pending mempool data; mitigate with commit-reveal
- Oracle manipulation- Relying on a single/spot-price DEX pool as a price oracle lets attackers flash-loan-manipulate it
- Unchecked delegatecall- delegatecall runs external code in the caller's storage context; can corrupt state if target is untrusted
- tx.origin authentication- Using tx.origin instead of msg.sender for auth is phishable via a malicious intermediary contract
Querying & Filtering Historical Events
Reading past on-chain activity efficiently instead of scanning every block manually.
import { ethers } from 'ethers';const provider = new ethers.JsonRpcProvider(RPC_URL);const contract = new ethers.Contract(address, abi, provider);// Query a specific indexed argument across a block rangeconst filter = contract.filters.Transfer(null, myAddress); // 'to' == myAddressconst events = await contract.queryFilter(filter, -5000, 'latest'); // last 5000 blocksfor (const evt of events) { console.log(evt.args.from, evt.args.value.toString(), evt.blockNumber);}// Live subscription -- fires as new blocks confirm matching logscontract.on(contract.filters.Transfer(), (from, to, value, event) => { console.log(`Live transfer: ${value} from ${from} to ${to}`);});// Always unsubscribe to avoid leaking WebSocket listeners// contract.off('Transfer');
Batching Reads with Multicall
Combining many contract calls into a single RPC round trip and single block context for consistency.
import { ethers } from 'ethers';const multicallAbi = ['function aggregate((address,bytes)[] calls) view returns (uint256 blockNumber, bytes[] returnData)'];const multicall = new ethers.Contract(MULTICALL3_ADDRESS, multicallAbi, provider);const iface = new ethers.Interface(['function balanceOf(address) view returns (uint256)']);const calls = [addr1, addr2, addr3].map((holder) => ({ target: tokenAddress, callData: iface.encodeFunctionData('balanceOf', [holder]),}));const { returnData } = await multicall.aggregate(calls.map((c) => [c.target, c.callData]));const balances = returnData.map((data) => iface.decodeFunctionResult('balanceOf', data)[0]);// One RPC call, one consistent block, instead of N sequential calls// that could each observe a different chain state
Always call view/pure functions through a read-only provider (free, instant) and reserve signed transactions for actual state changes — calling a state-changing function when only a read was needed wastes real gas for no reason.