Decentralized future — 6 lessons covering smart contracts, Solidity, DeFi protocols, DApp development, and security.
A blockchain is an immutable, distributed ledger where data is grouped into blocks, each containing a cryptographic hash of the previous block. This creates a chain that is tamper-evident — altering one block invalidates every subsequent block.
A cryptographic hash function maps arbitrary-length input to a fixed-length output. Blockchain uses SHA-256 (Bitcoin) and Keccak-256 (Ethereum) to link blocks and verify data integrity.
const crypto = require('crypto');
// SHA-256: Bitcoin's hash function
const block = { index: 1, data: "Alice sends 5 BTC to Bob", timestamp: 1700000000 };
const hash = crypto.createHash('sha256').update(JSON.stringify(block)).digest('hex');
console.log(hash);
// => "a3f2b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1"
// Keccak-256: Ethereum's hash function
const { keccak256 } = require('@noble/hashes/sha3');
const data = ethers.utils.toUtf8Bytes("Hello, Ethereum!");
const ethHash = ethers.utils.hexlify(keccak256(data));
console.log(ethHash);
Merkle trees allow efficient verification of large datasets. Each leaf is a transaction hash; the Merkle root is embedded in the block header. Verifying a single transaction requires only O(log n) hashes instead of O(n).
const { keccak256 } = require('@noble/hashes/sha3');
function merkleRoot(transactions) {
if (transactions.length === 0) return '0x' + '0'.repeat(64);
if (transactions.length === 1) return transactions[0];
let level = transactions.map(tx =>
ethers.utils.hexlify(keccak256(ethers.utils.toUtf8Bytes(tx)))
);
while (level.length > 1) {
const next = [];
for (let i = 0; i < level.length; i += 2) {
const left = level[i];
const right = i + 1 < level.length ? level[i + 1] : left;
const combined = ethers.utils.concat([left, right]);
next.push(ethers.utils.hexlify(keccak256(combined)));
}
level = next;
}
return level[0];
}
const txs = ["Alice->Bob:5", "Bob->Carol:3", "Carol->Dave:2", "Dave->Alice:1"];
console.log("Merkle Root:", merkleRoot(txs));
Miners compete to find a nonce that produces a hash below a difficulty target. The first to solve it broadcasts the block and earns a reward.
const crypto = require('crypto');
function proofOfWork(data, difficulty) {
let nonce = 0;
const prefix = '0'.repeat(difficulty);
while (true) {
const hash = crypto.createHash('sha256')
.update(data + nonce)
.digest('hex');
if (hash.startsWith(prefix)) {
return { nonce, hash };
}
nonce++;
}
}
const result = proofOfWork("block #1000", 4);
console.log(`Nonce: ${result.nonce}, Hash: ${result.hash}`);
Validators stake ETH as collateral. The protocol pseudo-randomly selects a validator to propose each block. Misbehavior results in slashing — partial or total loss of staked ETH. PoS uses ~99.95% less energy than PoW.
Consensus mechanisms solve the Byzantine Generals Problem — how to reach agreement among distributed, potentially malicious nodes. Key algorithms:
The EVM is a deterministic, sandboxed runtime that executes smart contract bytecode. Every Ethereum node runs the same code against the same state, producing identical results. It is a stack-based machine with 256-bit word size.
const { ethers } = require('ethers');
const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_KEY");
async function inspectAccount(address) {
const code = await provider.getCode(address);
const balance = await provider.getBalance(address);
const nonce = await provider.getTransactionCount(address);
console.log(`Address: ${address}`);
console.log(`Balance: ${ethers.formatEther(balance)} ETH`);
console.log(`Nonce: ${nonce}`);
console.log(`Is Contract: ${code !== '0x'}`);
}
inspectAccount("0xdAC17F958D2ee523a2206206994597C13D831ec7"); // USDT (contract)
inspectAccount("0x0000000000000000000000000000000000000000"); // Burn address
const { ethers } = require('ethers');
const provider = new ethers.JsonRpcProvider("https://mainnet.infura.io/v3/YOUR_KEY");
async function estimateGasCost() {
const feeData = await provider.getFeeData();
const gasLimit = 21000n;
const gasPrice = feeData.gasPrice;
const totalCostWei = gasLimit * gasPrice;
const totalCostEth = ethers.formatEther(totalCostWei);
console.log(`Gas Limit: ${gasLimit}`);
console.log(`Gas Price: ${ethers.formatUnits(gasPrice, 'gwei')} Gwei`);
console.log(`Max Fee: ${totalCostEth} ETH`);
console.log(`Max Fee (USD): $${(parseFloat(totalCostEth) * 3500).toFixed(2)}`);
}
estimateGasCost();
// EIP-1559: Base fee + Priority fee
async function eip1559Cost() {
const feeData = await provider.getFeeData();
console.log(`Base Fee: ${ethers.formatUnits(feeData.gasPrice, 'gwei')} Gwei`);
console.log(`Max Priority Fee: ${ethers.formatUnits(feeData.maxPriorityFeePerGas, 'gwei')} Gwei`);
console.log(`Max Fee Per Gas: ${ethers.formatUnits(feeData.maxFeePerGas, 'gwei')} Gwei`);
}
eip1559Cost();
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Counter {
uint256 public count;
address public owner;
event CountChanged(uint256 newCount, address indexed caller);
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_;
}
constructor() {
owner = msg.sender;
count = 0;
}
function increment() public {
count += 1;
emit CountChanged(count, msg.sender);
}
function decrement() public {
require(count > 0, "Count is already zero");
count -= 1;
emit CountChanged(count, msg.sender);
}
function getCount() public view returns (uint256) {
return count;
}
function reset() public onlyOwner {
count = 0;
emit CountChanged(count, msg.sender);
}
}
const { ethers } = require('ethers');
async function deployCounter() {
const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const abi = [
"function count() view returns (uint256)",
"function owner() view returns (address)",
"function increment()",
"function decrement()",
"function getCount() view returns (uint256)",
"function reset()"
];
const bytecode = "0x6080604052..."; // Compiled Solidity bytecode
const factory = new ethers.ContractFactory(abi, bytecode, wallet);
const contract = await factory.deploy();
await contract.waitForDeployment();
console.log(`Deployed to: ${await contract.getAddress()}`);
await contract.increment();
console.log(`Count: ${await contract.count()}`);
}
deployCounter();
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SolidityTypes {
// Value types
uint256 public uintVar = 1 ether;
int256 public intVar = -42;
bool public boolVar = true;
address public addrVar = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
bytes32 public bytes32Var = keccak256("hello");
// Reference types
string public stringVar = "Hello, Solidity";
bytes public bytesVar = abi.encodePacked("raw bytes");
// Arrays
uint256[] public dynamicArray;
uint256[5] public fixedArray;
// Mappings (hash tables)
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
// Structs
struct Proposal {
uint256 id;
string description;
uint256 voteCount;
bool executed;
mapping(address => bool) hasVoted;
}
Proposal[] public proposals;
// Enums
enum Status { Pending, Active, Closed, Cancelled }
Status public currentStatus;
function createProposal(string calldata _desc) external {
proposals.push(Proposal({
id: proposals.length,
description: _desc,
voteCount: 0,
executed: false
}));
}
// Storage vs Memory vs Calldata
function processArray(uint256[] calldata input) external pure returns (uint256) {
uint256 sum = 0;
for (uint256 i = 0; i < input.length; i++) {
sum += input[i];
}
return sum;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract AccessControl {
address public admin;
mapping(address => bool) public moderators;
mapping(address => uint256) public lastAction;
// Custom errors (cheaper than require strings)
error NotAdmin();
error NotModerator();
error CooldownActive(uint256 remaining);
event AdminChanged(address indexed oldAdmin, address indexed newAdmin);
event ModeratorAdded(address indexed account);
event ActionRecorded(address indexed user, uint256 timestamp);
modifier onlyAdmin() {
if (msg.sender != admin) revert NotAdmin();
_;
}
modifier onlyModerator() {
if (!moderators[msg.sender]) revert NotModerator();
_;
}
modifier cooldown(uint256 _seconds) {
uint256 elapsed = block.timestamp - lastAction[msg.sender];
if (elapsed < _seconds) {
revert CooldownActive(_seconds - elapsed);
}
_;
}
constructor() {
admin = msg.sender;
}
function addModerator(address _account) external onlyAdmin {
moderators[_account] = true;
emit ModeratorAdded(_account);
}
function changeAdmin(address _newAdmin) external onlyAdmin {
emit AdminChanged(admin, _newAdmin);
admin = _newAdmin;
}
function recordAction() external cooldown(1 hours) {
lastAction[msg.sender] = block.timestamp;
emit ActionRecorded(msg.sender, block.timestamp);
}
}
require strings. Use revert NotAdmin() instead of require(msg.sender == admin, "Not admin").// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
abstract contract Ownable {
address public owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor(address _owner) {
owner = _owner;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
function transferOwnership(address _newOwner) external onlyOwner {
emit OwnershipTransferred(owner, _newOwner);
owner = _newOwner;
}
}
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract Pausable is Ownable {
bool public paused;
event Paused(address account);
event Unpaused(address account);
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
modifier whenPaused() {
require(paused, "Contract is not paused");
_;
}
constructor(address _owner) Ownable(_owner) {}
function pause() external onlyOwner {
paused = true;
emit Paused(msg.sender);
}
function unpause() external onlyOwner {
paused = false;
emit Unpaused(msg.sender);
}
}
contract TokenVault is Pausable {
IERC20 public immutable token;
mapping(address => uint256) public deposits;
event Deposited(address indexed user, uint256 amount);
event Withdrawn(address indexed user, uint256 amount);
constructor(address _token) Pausable(msg.sender) {
token = IERC20(_token);
}
function deposit(uint256 _amount) external whenNotPaused {
token.transferFrom(msg.sender, address(this), _amount);
deposits[msg.sender] += _amount;
emit Deposited(msg.sender, _amount);
}
function withdraw(uint256 _amount) external whenNotPaused {
require(deposits[msg.sender] >= _amount, "Insufficient balance");
deposits[msg.sender] -= _amount;
token.transfer(msg.sender, _amount);
emit Withdrawn(msg.sender, _amount);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: overflow");
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SafeMath: underflow");
return a - b;
}
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) return 0;
uint256 c = a * b;
require(c / a == b, "SafeMath: overflow");
return c;
}
}
contract UsingSafeMath {
using SafeMath for uint256;
uint256 public total;
function addToTotal(uint256 _amount) external {
total = total.add(_amount);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
contract MyToken is IERC20 {
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 private _totalSupply;
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
_mint(msg.sender, _initialSupply * 10 ** decimals);
}
function totalSupply() external view override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) external view override returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) external override returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) external override returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) external override returns (bool) {
uint256 currentAllowance = _allowances[from][msg.sender];
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_allowances[from][msg.sender] = currentAllowance - amount;
}
_transfer(from, to, amount);
return true;
}
function _transfer(address from, address to, uint256 amount) internal {
require(from != address(0), "ERC20: transfer from zero");
require(to != address(0), "ERC20: transfer to zero");
require(_balances[from] >= amount, "ERC20: insufficient balance");
unchecked {
_balances[from] -= amount;
}
_balances[to] += amount;
emit Transfer(from, to, amount);
}
function _mint(address to, uint256 amount) internal {
require(to != address(0), "ERC20: mint to zero");
_totalSupply += amount;
_balances[to] += amount;
emit Transfer(address(0), to, amount);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IERC721 {
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
function balanceOf(address owner) external view returns (uint256);
function ownerOf(uint256 tokenId) external view returns (address);
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address);
function transferFrom(address from, address to, uint256 tokenId) external;
}
contract MyNFT is IERC721 {
string private _name;
string private _symbol;
uint256 private _tokenIdCounter;
uint256 public mintPrice = 0.01 ether;
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _tokenApprovals;
mapping(address => mapping(address => bool)) private _operatorApprovals;
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
function name() external view returns (string memory) { return _name; }
function symbol() external view returns (string memory) { return _symbol; }
function balanceOf(address owner) external view override returns (uint256) {
require(owner != address(0), "Zero address");
return _balances[owner];
}
function ownerOf(uint256 tokenId) public view override returns (address) {
address owner = _owners[tokenId];
require(owner != address(0), "Token does not exist");
return owner;
}
function mint(address to) external payable {
require(msg.value >= mintPrice, "Insufficient payment");
uint256 tokenId = _tokenIdCounter++;
_owners[tokenId] = to;
_balances[to] += 1;
emit Transfer(address(0), to, tokenId);
}
function approve(address to, uint256 tokenId) external override {
address owner = ownerOf(tokenId);
require(msg.sender == owner, "Not owner");
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
function getApproved(uint256 tokenId) external view override returns (address) {
return _tokenApprovals[tokenId];
}
function transferFrom(address from, address to, uint256 tokenId) external override {
address owner = ownerOf(tokenId);
require(
msg.sender == owner ||
msg.sender == _tokenApprovals[tokenId] ||
isApprovedForAll(owner, msg.sender),
"Not authorized"
);
_tokenApprovals[tokenId] = address(0);
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
}
function setApprovalForAll(address operator, bool approved) external {
_operatorApprovals[msg.sender][operator] = approved;
}
function isApprovedForAll(address owner, address operator) public view returns (bool) {
return _operatorApprovals[owner][operator];
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
contract SimpleAMM {
using SafeERC20 for IERC20;
IERC20 public immutable token0;
IERC20 public immutable token1;
uint256 public reserve0;
uint256 public reserve1;
uint256 public totalLP;
mapping(address => uint256) public lpBalances;
event LiquidityAdded(address indexed provider, uint256 amount0, uint256 amount1, uint256 lpTokens);
event Swapped(address indexed user, bool zeroForOne, uint256 amountIn, uint256 amountOut);
constructor(address _token0, address _token1) {
token0 = IERC20(_token0);
token1 = IERC20(_token1);
}
function addLiquidity(uint256 amount0, uint256 amount1) external returns (uint256 lpTokens) {
token0.safeTransferFrom(msg.sender, address(this), amount0);
token1.safeTransferFrom(msg.sender, address(this), amount1);
if (totalLP == 0) {
lpTokens = sqrt(amount0 * amount1);
} else {
lpTokens = min(
(amount0 * totalLP) / reserve0,
(amount1 * totalLP) / reserve1
);
}
totalLP += lpTokens;
lpBalances[msg.sender] += lpTokens;
reserve0 += amount0;
reserve1 += amount1;
emit LiquidityAdded(msg.sender, amount0, amount1, lpTokens);
}
function swap(bool zeroForOne, uint256 amountIn) external returns (uint256 amountOut) {
uint256 amountInWithFee = amountIn * 997;
uint256 reserveIn = zeroForOne ? reserve0 : reserve1;
uint256 reserveOut = zeroForOne ? reserve1 : reserve0;
amountOut = (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
require(amountOut > 0, "Insufficient output");
if (zeroForOne) {
token0.safeTransferFrom(msg.sender, address(this), amountIn);
token1.transfer(msg.sender, amountOut);
reserve0 += amountIn;
reserve1 -= amountOut;
} else {
token1.safeTransferFrom(msg.sender, address(this), amountIn);
token0.transfer(msg.sender, amountOut);
reserve1 += amountIn;
reserve0 -= amountOut;
}
emit Swapped(msg.sender, zeroForOne, amountIn, amountOut);
}
function getAmountOut(bool zeroForOne, uint256 amountIn) external view returns (uint256) {
uint256 amountInWithFee = amountIn * 997;
uint256 reserveIn = zeroForOne ? reserve0 : reserve1;
uint256 reserveOut = zeroForOne ? reserve1 : reserve0;
return (amountInWithFee * reserveOut) / (reserveIn * 1000 + amountInWithFee);
}
function sqrt(uint256 x) internal pure returns (uint256) {
if (x == 0) return 0;
uint256 z = (x + 1) / 2;
uint256 y = x;
while (z < y) { y = z; z = (x / z + z) / 2; }
return y;
}
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract Staking {
IERC20 public immutable stakingToken;
uint256 public rewardRate = 10;
uint256 public lastClaimBlock;
struct StakeInfo {
uint256 amount;
uint256 rewardDebt;
uint256 stakedAt;
}
mapping(address => StakeInfo) public stakes;
uint256 public totalStaked;
event Staked(address indexed user, uint256 amount, uint256 blockNumber);
event Withdrawn(address indexed user, uint256 amount);
event RewardClaimed(address indexed user, uint256 reward);
constructor(address _stakingToken) {
stakingToken = IERC20(_stakingToken);
}
function stake(uint256 _amount) external {
require(_amount > 0, "Zero amount");
stakingToken.transferFrom(msg.sender, address(this), _amount);
if (stakes[msg.sender].amount > 0) {
_claimReward(msg.sender);
}
stakes[msg.sender].amount += _amount;
stakes[msg.sender].stakedAt = block.number;
totalStaked += _amount;
emit Staked(msg.sender, _amount, block.number);
}
function withdraw(uint256 _amount) external {
StakeInfo storage info = stakes[msg.sender];
require(info.amount >= _amount, "Insufficient stake");
_claimReward(msg.sender);
info.amount -= _amount;
totalStaked -= _amount;
stakingToken.transfer(msg.sender, _amount);
emit Withdrawn(msg.sender, _amount);
}
function claimReward() external {
_claimReward(msg.sender);
}
function _claimReward(address _user) internal {
uint256 reward = pendingReward(_user);
if (reward > 0) {
stakes[_user].rewardDebt = (stakes[_user].amount * (block.number - stakes[_user].stakedAt) * rewardRate) / totalStaked;
stakingToken.transfer(_user, reward);
emit RewardClaimed(_user, reward);
}
}
function pendingReward(address _user) public view returns (uint256) {
StakeInfo storage info = stakes[_user];
if (info.amount == 0 || totalStaked == 0) return 0;
return (info.amount * (block.number - info.stakedAt) * rewardRate) / totalStaked;
}
}
async function connectWallet() {
if (!window.ethereum) {
alert("Please install MetaMask");
return null;
}
try {
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = accounts[0];
console.log(`Connected: ${address}`);
window.ethereum.on("accountsChanged", (newAccounts) => {
if (newAccounts.length === 0) {
console.log("Disconnected");
} else {
console.log(`Switched to: ${newAccounts[0]}`);
location.reload();
}
});
window.ethereum.on("chainChanged", () => {
location.reload();
});
return { provider, signer, address };
} catch (err) {
console.error("Connection failed:", err);
return null;
}
}
const CHAINS = {
ethereum: { chainId: "0x1", name: "Ethereum Mainnet" },
sepolia: { chainId: "0xaa36a7", name: "Sepolia Testnet" },
polygon: { chainId: "0x89", name: "Polygon Mainnet" },
arbitrum: { chainId: "0xa4b1", name: "Arbitrum One" },
};
async function switchChain(chainKey) {
const chain = CHAINS[chainKey];
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: chain.chainId }],
});
} catch (err) {
if (err.code === 4902) {
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: chain.chainId,
chainName: chain.name,
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
rpcUrls: ["https://rpc.ankr.com/eth"],
blockExplorerUrls: ["https://etherscan.io"],
}],
});
}
}
}
const TOKEN_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"function approve(address spender, uint256 amount) returns (bool)",
"function allowance(address owner, address spender) view returns (uint256)",
];
async function readTokenInfo(tokenAddress) {
const provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(tokenAddress, TOKEN_ABI, provider);
const [name, symbol, decimals, totalSupply] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals(),
contract.totalSupply(),
]);
console.log(`${name} (${symbol})`);
console.log(`Decimals: ${decimals}`);
console.log(`Total Supply: ${ethers.formatUnits(totalSupply, decimals)}`);
}
async function checkBalance(tokenAddress, walletAddress) {
const provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(tokenAddress, TOKEN_ABI, provider);
const balance = await contract.balanceOf(walletAddress);
const decimals = await contract.decimals();
const formatted = ethers.formatUnits(balance, decimals);
console.log(`Balance: ${formatted} ${await contract.symbol()}`);
return { raw: balance, formatted };
}
view functions for reads (no gas) and transactions for writes. Estimate gas before sending to avoid failed transactions.async function transferTokens(tokenAddress, toAddress, amountEth) {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const contract = new ethers.Contract(tokenAddress, TOKEN_ABI, signer);
const decimals = await contract.decimals();
const amount = ethers.parseUnits(amountEth, decimals);
const gasEstimate = await contract.transfer.estimateGas(toAddress, amount);
const feeData = await provider.getFeeData();
const tx = await contract.transfer(toAddress, amount, {
gasLimit: gasEstimate * 120n / 100n,
maxFeePerGas: feeData.maxFeePerGas,
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
});
console.log(`TX sent: ${tx.hash}`);
const receipt = await tx.wait();
console.log(`Confirmed in block ${receipt.blockNumber}`);
console.log(`Gas used: ${receipt.gasUsed.toString()}`);
return receipt;
}
import { useState, useEffect, useCallback } from "react";
import { ethers } from "ethers";
const CONTRACT_ADDRESS = "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4";
const ABI = [
"function count() view returns (uint256)",
"function increment()",
"function decrement()",
"event CountChanged(uint256 newCount, address indexed caller)",
];
export default function CounterDApp() {
const [account, setAccount] = useState(null);
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
const [contract, setContract] = useState(null);
const connect = useCallback(async () => {
if (!window.ethereum) return alert("Install MetaMask");
const _provider = new ethers.BrowserProvider(window.ethereum);
const _signer = await _provider.getSigner();
const _contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, _signer);
const _account = await _signer.getAddress();
setContract(_contract);
setAccount(_account);
const _count = await _contract.count();
setCount(Number(_count));
}, []);
const increment = async () => {
if (!contract) return;
setLoading(true);
try {
const tx = await contract.increment();
await tx.wait();
const newCount = await contract.count();
setCount(Number(newCount));
} catch (err) {
console.error(err);
}
setLoading(false);
};
const decrement = async () => {
if (!contract) return;
setLoading(true);
try {
const tx = await contract.decrement();
await tx.wait();
const newCount = await contract.count();
setCount(Number(newCount));
} catch (err) {
console.error(err);
}
setLoading(false);
};
useEffect(() => {
if (!contract) return;
const handler = (newCount, caller) => {
setCount(Number(newCount));
console.log(`Count changed by ${caller}`);
};
contract.on("CountChanged", handler);
return () => contract.off("CountChanged", handler);
}, [contract]);
if (!account) {
return (
Counter DApp
);
}
return (
Counter DApp
Account: {account.slice(0, 6)}...{account.slice(-4)}
Count: {count}
{loading && Transaction pending...
}
);
}
async function sendAndWait(txPromise) {
const tx = await txPromise;
console.log(`Pending: ${tx.hash}`);
const receipt = await tx.wait(1);
if (receipt.status === 1) {
console.log(`Success in block ${receipt.blockNumber}`);
console.log(`Gas used: ${receipt.gasUsed}`);
console.log(`Events:`, receipt.logs.map(log => ({
topic: log.topics[0],
data: log.data,
})));
} else {
console.error("Transaction reverted");
}
return receipt;
}
async function batchReads(provider, contract, addresses) {
const calls = addresses.map(addr =>
contract.balanceOf(addr).then(bal => ({ address: addr, balance: bal }))
);
return Promise.all(calls);
}
window.ethereum for browser-based wallet interactionview functions for reads (no gas) and transactions for writesThe most devastating Solidity vulnerability. A malicious contract calls back into the vulnerable contract before the first execution finishes, draining funds.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
// VULNERABLE — DO NOT USE
contract VulnerableVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 balance = balances[msg.sender];
require(balance > 0, "No balance");
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Transfer failed");
balances[msg.sender] = 0;
}
}
// Attack contract
contract ReentrancyAttacker {
VulnerableVault public vault;
constructor(address _vault) {
vault = VulnerableVault(_vault);
}
function attack() external payable {
vault.deposit{value: msg.value}();
vault.withdraw();
}
receive() external payable {
if (address(vault).balance >= 1 ether) {
vault.withdraw();
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
abstract contract ReentrancyGuard {
uint256 private _locked = 1;
modifier nonReentrant() {
require(_locked == 1, "ReentrancyGuard: reentrant call");
_locked = 2;
_;
_locked = 1;
}
}
contract SecureVault is ReentrancyGuard {
mapping(address => uint256) public balances;
event Deposit(address indexed user, uint256 amount);
event Withdrawal(address indexed user, uint256 amount);
function deposit() external payable {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw() external nonReentrant {
uint256 balance = balances[msg.sender];
require(balance > 0, "No balance");
balances[msg.sender] = 0;
emit Withdrawal(msg.sender, balance);
(bool success, ) = msg.sender.call{value: balance}("");
require(success, "Transfer failed");
}
}
contract OverflowExample {
uint8 public counter = 255;
function safeIncrement() external {
counter += 1; // Reverts on overflow (Solidity 0.8+)
}
function unsafeIncrement() external {
unchecked {
counter += 1; // Wraps to 0!
}
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract GoodOwnership {
address public owner;
bool private _initialized;
constructor() {
require(!_initialized, "Already initialized");
owner = msg.sender;
_initialized = true;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
address public pendingOwner;
function transferOwnership(address newOwner) external onlyOwner {
pendingOwner = newOwner;
}
function acceptOwnership() external {
require(msg.sender == pendingOwner, "Not pending owner");
owner = pendingOwner;
pendingOwner = address(0);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract CommitReveal {
mapping(address => bytes32) public commitments;
mapping(address => uint256) public commitmentBlocks;
event Committed(address indexed user, bytes32 hash);
event Revealed(address indexed user, uint256 value);
function commit(bytes32 _hash) external {
commitments[msg.sender] = _hash;
commitmentBlocks[msg.sender] = block.number;
emit Committed(msg.sender, _hash);
}
function reveal(uint256 _value, bytes32 _salt) external {
require(
block.number > commitmentBlocks[msg.sender] + 2,
"Wait 3 blocks"
);
bytes32 hash = keccak256(abi.encodePacked(_value, _salt, msg.sender));
require(hash == commitments[msg.sender], "Invalid reveal");
delete commitments[msg.sender];
emit Revealed(msg.sender, _value);
}
}
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: {
version: "0.8.20",
settings: {
optimizer: { enabled: true, runs: 200 },
},
},
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL,
accounts: [process.env.PRIVATE_KEY],
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
};
// test/Counter.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Counter", function () {
let counter, owner, addr1;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
const Counter = await ethers.getContractFactory("Counter");
counter = await Counter.deploy();
});
it("should start at zero", async function () {
expect(await counter.count()).to.equal(0);
});
it("should increment", async function () {
await counter.increment();
expect(await counter.count()).to.equal(1);
});
it("should decrement from positive", async function () {
await counter.increment();
await counter.increment();
await counter.decrement();
expect(await counter.count()).to.equal(1);
});
it("should revert when decrementing from zero", async function () {
await expect(counter.decrement()).to.be.revertedWith("Count is already zero");
});
it("should emit CountChanged event", async function () {
await expect(counter.increment())
.to.emit(counter, "CountChanged")
.withArgs(1, owner.address);
});
it("only owner can reset", async function () {
await counter.increment();
await counter.reset();
expect(await counter.count()).to.equal(0);
await expect(counter.connect(addr1).reset()).to.be.revertedWith("Not the owner");
});
});
pip install slither-analyzer
slither contracts/Vault.sol
slither contracts/Vault.sol --filter-paths node_modules
# Common detectors:
# reentrancy-eth — Reentrancy with ETH transfers
# reentrancy-no-eth — Reentrancy with contract calls
# unchecked-transfer — ERC20 transfer return value ignored
# locked-ether — Contract receives ETH but cannot withdraw
# arbitrary-send — Unrestricted ETH withdrawal
forge init my-project
cd my-project
forge build
forge test
# Fuzz test
forge test --match-test testFuzzStake -vvvv
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Staking.sol";
contract StakingTest is Test {
Staking public staking;
MockERC20 public token;
function setUp() public {
token = new MockERC20("StakeToken", "STK", 1000 ether);
staking = new Staking(address(token));
token.mint(address(this), 1000 ether);
token.approve(address(staking), type(uint256).max);
}
function testStake() public {
staking.stake(100 ether);
assertEq(staking.totalStaked(), 100 ether);
assertEq(staking.stakes(address(this)).amount, 100 ether);
}
function testWithdraw() public {
staking.stake(100 ether);
staking.withdraw(50 ether);
assertEq(staking.totalStaked(), 50 ether);
}
function testWithdrawRevertsOnInsufficientBalance() public {
staking.stake(100 ether);
vm.expectRevert("Insufficient stake");
staking.withdraw(200 ether);
}
function testFuzzStake(uint256 amount) public {
vm.assume(amount > 0 && amount <= 1000 ether);
staking.stake(amount);
assertEq(staking.totalStaked(), amount);
}
}
contract MockERC20 {
string public name;
string public symbol;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
_mint(msg.sender, _initialSupply);
}
function mint(address to, uint256 amount) external {
totalSupply += amount;
balanceOf[to] += amount;
}
function approve(address spender, uint256 amount) external returns (bool) {
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
balanceOf[from] -= amount;
balanceOf[to] += amount;
return true;
}
}
msg.sender for every state-changing function