Introduction
Smart contracts are self-executing programs stored on a blockchain that automatically enforce and execute the terms of an agreement when predetermined conditions are met. They form the backbone of decentralized applications (dApps), DeFi protocols, NFT marketplaces, and DAOs.
This comprehensive guide covers everything from fundamentals to production deployment, including Solidity programming, security best practices, gas optimization, testing, and real-world use cases on Ethereum and EVM-compatible blockchains.
A Brief History
The term “smart contract” was coined by cryptographer Nick Szabo in 1994. He used the vending machine analogy: insert money, select product, machine dispenses automatically — a contract embedded in hardware, no intermediary needed. However, the technology to implement this on a decentralized network didn’t exist until Ethereum.
In late 2013, Vitalik Buterin published the Ethereum whitepaper, proposing a blockchain as a “world computer” for arbitrary programs. Ethereum launched on July 30, 2015, and has since become the dominant smart contract platform, hosting over 55-60% of DeFi total value locked.
How Smart Contracts Work on Ethereum
The Ethereum Virtual Machine (EVM)
The EVM is a stack-based virtual machine that executes smart contract bytecode. Key characteristics:
- Stack-based: LIFO stack with max depth 1024, each item 256 bits (32 bytes)
- Deterministic: Every node produces exactly the same result for the same transaction
- Isolated: Contracts run in sandboxed environments, no filesystem or network access
- Gas-metered: Every operation costs gas, preventing infinite loops and compensating validators
From Source Code to Blockchain
- Source Code: Written in Solidity (or Vyper)
- Compilation: The
solccompiler produces bytecode (EVM opcodes) and an ABI (JSON interface for external interaction) - Deployment: A transaction with no
toaddress, containing bytecode in thedatafield. The constructor runs, and runtime bytecode is stored at the new contract address. - Interaction: Users call functions by sending transactions with ABI-encoded calldata. The function selector (first 4 bytes of keccak256 hash) routes to the correct function.
Gas: The Fuel of Ethereum
Gas measures computational effort. Since EIP-1559 (August 2021), the fee model works as:
- Base fee: Set by the protocol, adjusts dynamically based on demand. This fee is burned.
- Priority fee (tip): Optional tip to validators for faster inclusion.
- Total cost:
gas_used × (base_fee + priority_fee)
Typical costs: ETH transfer ~21,000 gas, ERC-20 transfer ~45-65K gas, Uniswap swap ~100-150K gas.
Solidity Fundamentals
Data Types
Solidity offers value types and reference types:
// Value types
uint256 count = 42; // Unsigned integer (0 to 2^256-1)
int256 balance = -100; // Signed integer
bool isActive = true;
address owner = msg.sender; // 20-byte Ethereum address
address payable recipient; // Can receive ETH via .transfer()
bytes32 hash; // Fixed-size byte array
// Reference types
string name = "MyToken"; // Dynamic UTF-8 string
bytes data; // Dynamic byte array
uint256[] numbers; // Dynamic array
uint256[5] fixed; // Fixed-size array
// Mapping - hash table, storage only, not iterable
mapping(address => uint256) public balances;
mapping(address => mapping(address => uint256)) public allowances;
// Struct - composite type
struct Proposal {
uint256 id;
address proposer;
uint256 voteCount;
bool executed;
}
// Enum
enum Status { Pending, Active, Completed }
Storage vs. Memory vs. Calldata
- Storage: Persistent on-chain data. Expensive: 20,000 gas for new slot, 5,000 to update, 2,100 to read (cold). State variables live here.
- Memory: Temporary, exists only during function execution. Much cheaper (3 gas per word). Used for function parameters and local variables.
- Calldata: Read-only input data for external functions. Cheapest for reading. Cannot be modified. Use instead of
memoryfor read-only parameters.
Functions and Visibility
contract MyContract {
address public owner;
// Visibility: public, private, internal, external
// Mutability: view, pure, payable, (default: nonpayable)
function transfer(address to, uint256 amount) external {
// external: only callable from outside
}
function _internalHelper() internal view returns (uint256) {
// internal: this contract + derived contracts
return balances[msg.sender];
}
function _secretLogic() private pure returns (uint256) {
// private: only this contract
return 42;
}
// Custom modifier for reusable access control
modifier onlyOwner() {
require(msg.sender == owner, "Not the owner");
_; // Function body goes here
}
function mint(address to, uint256 amount) external onlyOwner {
// Only owner can call this
}
// Special functions
constructor() { owner = msg.sender; } // Runs once at deployment
receive() external payable { } // Called on plain ETH transfers
fallback() external payable { } // Called when no function matches
}
Events and Error Handling
// Events - stored in transaction logs, cheap, for off-chain consumption
event Transfer(address indexed from, address indexed to, uint256 value);
emit Transfer(msg.sender, to, amount); // indexed params enable efficient filtering
// Error handling
require(balance >= amount, "Insufficient balance"); // Input validation
assert(totalSupply == computedTotal); // Internal invariants
// Custom errors (Solidity 0.8.4+) - gas efficient
error InsufficientBalance(uint256 requested, uint256 available);
if (balance < amount) revert InsufficientBalance(amount, balance);
Development Environment Setup
Hardhat (JavaScript/TypeScript)
mkdir my-project && cd my-project
npm init -y
npm install --save-dev hardhat
npx hardhat init # Choose TypeScript project
# Key commands
npx hardhat compile
npx hardhat test
npx hardhat node # Local development chain
npx hardhat run scripts/deploy.js --network sepolia
npx hardhat verify --network sepolia CONTRACT_ADDRESS
Hardhat’s killer feature: console.log for Solidity — import "hardhat/console.sol"; then console.log("Balance: %s", balance);
Foundry (Rust-based, Tests in Solidity)
curl -L https://foundry.paradigm.xyz | bash && foundryup
forge init my-project && cd my-project
# Key commands
forge build
forge test -vvvv # Maximum verbosity with traces
forge test --match-test testTransfer
forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast
Foundry advantages: 10-100x faster than Hardhat, built-in fuzz testing, invariant testing, and tests written in Solidity (closer to actual execution).
OpenZeppelin Contracts
The industry-standard library of audited, reusable components (v5.x). Key modules: ERC20, ERC721, ERC1155, Ownable, AccessControl, ReentrancyGuard, Governor, and proxy/upgrade contracts.
npm install @openzeppelin/contracts # Hardhat
forge install OpenZeppelin/openzeppelin-contracts # Foundry
Practical Example: ERC-20 Token
Here is a complete ERC-20 token using modern Solidity best practices:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
contract MyToken is ERC20, ERC20Burnable, ERC20Pausable, Ownable, ERC20Permit {
constructor(address initialOwner)
ERC20("MyToken", "MTK")
Ownable(initialOwner)
ERC20Permit("MyToken")
{
_mint(msg.sender, 1_000_000 * 10 ** decimals());
}
function pause() public onlyOwner { _pause(); }
function unpause() public onlyOwner { _unpause(); }
function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); }
function _update(address from, address to, uint256 value)
internal override(ERC20, ERC20Pausable)
{
super._update(from, to, value);
}
}
Security Vulnerabilities and Prevention
Reentrancy Attacks
The most infamous attack vector. In June 2016, “The DAO” lost ~$60M in ETH when an attacker exploited reentrancy in the splitDAO function, leading to Ethereum’s controversial hard fork.
// VULNERABLE - external call BEFORE state update
function withdraw() external {
uint256 balance = balances[msg.sender];
(bool success, ) = msg.sender.call{value: balance}(""); // Attacker re-enters here!
balances[msg.sender] = 0; // Too late - already drained
}
// SAFE - Checks-Effects-Interactions (CEI) pattern
function withdraw() external nonReentrant {
uint256 balance = balances[msg.sender]; // CHECK
balances[msg.sender] = 0; // EFFECT (update state first)
(bool success, ) = msg.sender.call{value: balance}(""); // INTERACTION (last)
require(success);
}
Always use the CEI pattern and OpenZeppelin’s ReentrancyGuard (nonReentrant modifier).
Integer Overflow/Underflow
Before Solidity 0.8, arithmetic silently wrapped: uint8(255) + 1 = 0. The BEC token was exploited this way in 2018. Solidity 0.8+ reverts on overflow by default. Use unchecked {} only when overflow is provably impossible (for gas savings).
Front-Running and MEV
MEV (Maximal Extractable Value) bots reorder transactions for profit. Sandwich attacks place buy/sell orders around victim trades. Prevention: use Flashbots Protect (private transaction submission), batch auctions (CoW Protocol), and slippage protection on DEX trades.
Access Control Issues
- Never use
tx.originfor authentication (vulnerable to phishing via malicious contracts) — usemsg.sender - Use
Ownable2Step(requires new owner to accept) instead ofOwnable - Use multi-sig wallets (Safe) for admin keys
- Implement timelocks for critical administrative actions
Flash Loan Attacks
Uncollateralized loans borrowed and repaid in one transaction. Attackers use massive borrowed amounts to manipulate price oracles. Prevention: use Chainlink price feeds (not DEX spot prices), TWAPs from Uniswap V3, and single-block manipulation detection.
Gas Optimization
- Variable packing: Group smaller types together so multiple variables fit in one 32-byte storage slot
- Cache storage reads: Read a storage variable once into memory instead of multiple SLOADs (2,100 gas each cold)
- Use
immutableandconstant: Zero storage cost — values stored in bytecode - Use
uncheckedblocks: Skip overflow checks when safe (e.g., loop counters) - Custom errors over require strings: 4-byte selector vs. full string encoding
- Use
calldataovermemory: For read-only external function parameters - Use events for non-critical data: Logs cost ~375 + 8/byte vs. 20,000 for storage
- Enable the optimizer:
optimizer: { enabled: true, runs: 200 }andviaIR: true
Testing Smart Contracts
Unit Tests (Foundry/Solidity)
contract MyTokenTest is Test {
MyToken public token;
function setUp() public {
token = new MyToken("MyToken", "MTK", 1_000_000);
}
function test_Transfer() public {
address alice = makeAddr("alice");
token.transfer(alice, 100e18);
assertEq(token.balanceOf(alice), 100e18);
}
function test_RevertWhen_InsufficientBalance() public {
vm.prank(makeAddr("alice")); // Set msg.sender
vm.expectRevert();
token.transfer(address(this), 1e18);
}
// Fuzz testing - automatic random inputs
function testFuzz_TransferConservesSupply(address to, uint256 amount) public {
vm.assume(to != address(0) && to != address(this));
amount = bound(amount, 0, token.balanceOf(address(this)));
uint256 totalBefore = token.totalSupply();
token.transfer(to, amount);
assertEq(token.totalSupply(), totalBefore);
}
}
Static Analysis
# Slither (by Trail of Bits) - most popular
pip install slither-analyzer && slither .
# Mythril (symbolic execution)
pip install mythril && myth analyze contracts/MyToken.sol
Deployment Process
- Test on Sepolia (primary Ethereum testnet). Get test ETH from faucets.
- Get audited before mainnet. Top firms: Trail of Bits, OpenZeppelin, Spearbit, Cyfrin.
- Set up a bug bounty on Immunefi.
- Deploy: Use a Safe multi-sig as deployer/owner, not an EOA.
- Verify on Etherscan:
npx hardhat verify --network mainnet ADDRESS
Upgrade Patterns
Smart contracts are immutable, but the proxy pattern enables upgrades. A proxy contract delegatecalls to an implementation contract — executing the implementation’s code with the proxy’s storage.
UUPS (Recommended)
Upgrade logic lives in the implementation. Lower gas per call, smaller proxy. The industry-standard approach.
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
contract MyTokenV1 is ERC20Upgradeable, OwnableUpgradeable, UUPSUpgradeable {
function initialize(address owner) public initializer {
__ERC20_init("MyToken", "MTK");
__Ownable_init(owner);
__UUPSUpgradeable_init();
}
function _authorizeUpgrade(address) internal override onlyOwner {}
}
// V2: add new features, append new state variables at the end
contract MyTokenV2 is MyTokenV1 {
function burn(uint256 amount) external { _burn(msg.sender, amount); }
uint256 public burnCount; // New state - must be at end
}
Other Patterns
- Transparent Proxy: Upgrade logic in the proxy itself. Extra gas per call due to admin check.
- Beacon Proxy: Multiple proxies share one beacon. Upgrading the beacon upgrades all proxies simultaneously.
- Diamond (EIP-2535): Multi-facet proxy delegating to multiple implementations. Overcomes the 24KB contract size limit.
Real-World Use Cases
DeFi (Decentralized Finance)
DeFi represents $80-120B+ in TVL. Key protocols:
- Uniswap: The dominant DEX using automated market makers (AMM). V3 introduced concentrated liquidity; V4 added customizable “hooks” and a singleton contract architecture.
- Aave: Leading lending protocol. V3 features cross-chain liquidity, efficiency mode for correlated assets, and the GHO stablecoin.
- Lido: Liquid staking for ETH (~$15-30B TVL). Deposit ETH, receive stETH that accrues staking rewards.
- MakerDAO (Sky): Creator of DAI, the largest decentralized stablecoin, using collateralized debt positions.
NFTs (Non-Fungible Tokens)
- ERC-721: Each token has a unique ID and single owner. Used for digital art, gaming, virtual real estate, ENS domains.
- ERC-1155: Multi-token standard — fungible and non-fungible in one contract. Batch operations, gas-efficient. Ideal for gaming.
- ERC-6551: Token Bound Accounts — every NFT gets its own smart contract wallet that can own assets.
DAOs (Decentralized Autonomous Organizations)
Organizations governed by smart contracts via token-weighted voting:
- Governance Token (ERC-20 with
ERC20Votes) for voting power - Governor Contract manages proposals — creation, voting, execution
- Timelock Controller adds a delay between approval and execution for community safety
Notable DAOs: Uniswap DAO (protocol governance), Aave DAO (risk parameters), Arbitrum DAO (ecosystem governance), Nouns DAO (public goods funding).
Looking Ahead: Account Abstraction (ERC-4337)
Deployed on Ethereum mainnet in March 2023, ERC-4337 allows smart contract wallets to initiate transactions without EOAs. Key features: paymasters (sponsor gas for users), flexible signature schemes (social recovery, passkeys), and session keys. This is the foundation for mainstream blockchain UX.
Conclusion
Smart contract development has matured significantly from Ethereum’s early days. Modern tooling (Hardhat, Foundry), battle-tested libraries (OpenZeppelin), and established security practices make it possible to build robust decentralized applications. Start with the fundamentals, prioritize security from day one, test extensively, get audited, and build on the shoulders of the open-source ecosystem.