Smart Contract Development: From Zero to Hero
⏱️ Duration
45 min
Introduction to Smart Contract Development
Smart contracts are self-executing programs that run on blockchain networks. This comprehensive guide will take you from basic concepts to deploying your own smart contracts on Ethereum.
Prerequisites
Before starting, you should understand:
- Basic programming concepts
- Blockchain fundamentals
- How Ethereum works
- Command line basics
Setting Up Your Development Environment
Required Tools
- Node.js & npm: JavaScript runtime
- Hardhat: Development framework
- MetaMask: Wallet for testing
- VS Code: Code editor
- Solidity Extension: Syntax highlighting
Installation Steps
# Install Node.js (v16 or higher)
# Download from nodejs.org
# Create project directory
mkdir my-first-dapp
cd my-first-dapp
# Initialize npm project
npm init -y
# Install Hardhat
npm install --save-dev hardhat
# Initialize Hardhat project
npx hardhat
# Install additional dependencies
npm install --save-dev @nomicfoundation/hardhat-toolbox
Solidity Fundamentals
Basic Syntax
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract HelloWorld {
// State variable
string public greeting = "Hello, World!";
// Function to change greeting
function setGreeting(string memory _greeting) public {
greeting = _greeting;
}
// Function to get greeting
function getGreeting() public view returns (string memory) {
return greeting;
}
}
Data Types
contract DataTypes {
// Value Types
bool public isActive = true;
uint256 public count = 123;
int256 public balance = -456;
address public owner = 0x123...;
bytes32 public data;
// Reference Types
string public name = "Alice";
uint[] public numbers;
mapping(address => uint) public balances;
// Struct
struct User {
string name;
uint age;
address wallet;
}
// Enum
enum Status { Pending, Active, Inactive }
}
Functions and Modifiers
contract FunctionExamples {
address public owner;
uint public value;
constructor() {
owner = msg.sender;
}
// Modifier
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
// View function (reads state)
function getValue() public view returns (uint) {
return value;
}
// Pure function (no state access)
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
// State-changing function
function setValue(uint _value) public onlyOwner {
value = _value;
}
// Payable function
function deposit() public payable {
// Receives ETH
}
}
Building Your First Smart Contract
Simple Token Contract (ERC-20)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, 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 sender, address recipient, 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 SimpleToken is IERC20 {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
string public name;
string public symbol;
uint8 public decimals;
constructor(string memory _name, string memory _symbol, uint256 _supply) {
name = _name;
symbol = _symbol;
decimals = 18;
_totalSupply = _supply * 10**decimals;
_balances[msg.sender] = _totalSupply;
emit Transfer(address(0), msg.sender, _totalSupply);
}
function totalSupply() public view override returns (uint256) {
return _totalSupply;
}
function balanceOf(address account) public view override returns (uint256) {
return _balances[account];
}
function transfer(address recipient, uint256 amount) public override returns (bool) {
require(_balances[msg.sender] >= amount, "Insufficient balance");
_balances[msg.sender] -= amount;
_balances[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
function approve(address spender, uint256 amount) public override returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function allowance(address owner, address spender) public view override returns (uint256) {
return _allowances[owner][spender];
}
function transferFrom(address sender, address recipient, uint256 amount) public override returns (bool) {
require(_balances[sender] >= amount, "Insufficient balance");
require(_allowances[sender][msg.sender] >= amount, "Insufficient allowance");
_balances[sender] -= amount;
_balances[recipient] += amount;
_allowances[sender][msg.sender] -= amount;
emit Transfer(sender, recipient, amount);
return true;
}
}
Advanced Concepts
Inheritance and Interfaces
// Interface
interface IOwnable {
function owner() external view returns (address);
function transferOwnership(address newOwner) external;
}
// Abstract contract
abstract contract Ownable is IOwnable {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
constructor() {
_owner = msg.sender;
}
function owner() public view override returns (address) {
return _owner;
}
modifier onlyOwner() {
require(owner() == msg.sender, "Ownable: caller is not the owner");
_;
}
function transferOwnership(address newOwner) public override onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
address oldOwner = _owner;
_owner = newOwner;
emit OwnershipTransferred(oldOwner, newOwner);
}
}
// Inheriting contract
contract MyContract is Ownable {
uint public value;
function setValue(uint _value) public onlyOwner {
value = _value;
}
}
Events and Logging
contract EventExample {
event ValueChanged(address indexed user, uint oldValue, uint newValue);
event Deposit(address indexed from, uint amount);
uint public value;
function setValue(uint _newValue) public {
uint oldValue = value;
value = _newValue;
emit ValueChanged(msg.sender, oldValue, _newValue);
}
receive() external payable {
emit Deposit(msg.sender, msg.value);
}
}
Error Handling
contract ErrorHandling {
error InsufficientBalance(uint requested, uint available);
error Unauthorized();
mapping(address => uint) public balances;
address public owner;
constructor() {
owner = msg.sender;
}
function withdraw(uint amount) public {
if (msg.sender != owner) {
revert Unauthorized();
}
if (amount > balances[msg.sender]) {
revert InsufficientBalance({
requested: amount,
available: balances[msg.sender]
});
}
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}
}
Security Best Practices
Common Vulnerabilities
- Reentrancy Attacks
// Vulnerable
contract Vulnerable {
mapping(address => uint) public balances;
function withdraw() public {
uint balance = balances[msg.sender];
(bool success, ) = msg.sender.call{value: balance}("");
require(success);
balances[msg.sender] = 0; // State change after call
}
}
// Secure
contract Secure {
mapping(address => uint) public balances;
function withdraw() public {
uint balance = balances[msg.sender];
balances[msg.sender] = 0; // State change before call
(bool success, ) = msg.sender.call{value: balance}("");
require(success);
}
}
- Integer Overflow/Underflow
// Use SafeMath or Solidity 0.8+ automatic checks
contract SafeContract {
uint public value;
function increment() public {
value += 1; // Automatically checks for overflow in Solidity 0.8+
}
}
- Access Control
contract AccessControl {
mapping(address => bool) public admins;
modifier onlyAdmin() {
require(admins[msg.sender], "Not admin");
_;
}
function sensitiveFunction() public onlyAdmin {
// Protected function
}
}
Testing Smart Contracts
Writing Tests with Hardhat
// test/Token.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Token Contract", function () {
let Token, token, owner, addr1, addr2;
beforeEach(async function () {
Token = await ethers.getContractFactory("SimpleToken");
[owner, addr1, addr2] = await ethers.getSigners();
token = await Token.deploy("MyToken", "MTK", 1000000);
});
describe("Deployment", function () {
it("Should set the right owner", async function () {
expect(await token.balanceOf(owner.address)).to.equal(
ethers.utils.parseEther("1000000")
);
});
it("Should have correct name and symbol", async function () {
expect(await token.name()).to.equal("MyToken");
expect(await token.symbol()).to.equal("MTK");
});
});
describe("Transactions", function () {
it("Should transfer tokens between accounts", async function () {
await token.transfer(addr1.address, 50);
expect(await token.balanceOf(addr1.address)).to.equal(50);
});
it("Should fail if sender doesn't have enough tokens", async function () {
const initialOwnerBalance = await token.balanceOf(owner.address);
await expect(
token.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("Insufficient balance");
expect(await token.balanceOf(owner.address)).to.equal(
initialOwnerBalance
);
});
});
});
Running Tests
# Run all tests
npx hardhat test
# Run specific test file
npx hardhat test test/Token.test.js
# Run with gas reporting
REPORT_GAS=true npx hardhat test
Deploying Smart Contracts
Deployment Script
// scripts/deploy.js
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
console.log("Account balance:", (await deployer.getBalance()).toString());
const Token = await ethers.getContractFactory("SimpleToken");
const token = await Token.deploy("MyToken", "MTK", 1000000);
await token.deployed();
console.log("Token deployed to:", token.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Network Configuration
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.19",
networks: {
hardhat: {
chainId: 1337
},
goerli: {
url: process.env.GOERLI_RPC_URL,
accounts: [process.env.PRIVATE_KEY]
},
mainnet: {
url: process.env.MAINNET_RPC_URL,
accounts: [process.env.PRIVATE_KEY]
}
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY
}
};
Gas Optimization Techniques
Storage Optimization
contract GasOptimized {
// Pack struct variables
struct User {
uint128 balance; // 16 bytes
uint64 timestamp; // 8 bytes
uint64 nonce; // 8 bytes
// Total: 32 bytes (1 storage slot)
}
// Use mappings instead of arrays when possible
mapping(address => User) public users;
// Use events for data that doesn't need on-chain storage
event DataLogged(address indexed user, string data);
// Cache storage variables
function efficientFunction() public {
User storage user = users[msg.sender];
uint128 cachedBalance = user.balance; // Cache to memory
// Use cachedBalance for multiple operations
if (cachedBalance > 100) {
// ...
}
}
}
Frontend Integration
Web3.js Example
// Connect to contract
const Web3 = require('web3');
const web3 = new Web3(window.ethereum);
const contractABI = [...]; // Your contract ABI
const contractAddress = '0x...'; // Your contract address
const contract = new web3.eth.Contract(contractABI, contractAddress);
// Read from contract
async function getBalance(address) {
const balance = await contract.methods.balanceOf(address).call();
return balance;
}
// Write to contract
async function transfer(to, amount) {
const accounts = await web3.eth.getAccounts();
await contract.methods.transfer(to, amount).send({ from: accounts[0] });
}
Tools and Resources
Development Tools
- Remix IDE: Browser-based Solidity IDE
- Truffle Suite: Development framework
- Foundry: Fast, portable toolkit
- OpenZeppelin: Secure contract libraries
Testing Tools
- Tenderly: Debugging and monitoring
- Ganache: Local blockchain
- Hardhat Network: Built-in network for testing
Security Tools
- Slither: Static analysis
- Mythril: Security analysis
- Echidna: Fuzzing tool
Next Steps
- Build a simple DeFi protocol
- Create an NFT collection
- Implement a DAO voting system
- Study advanced patterns
- Audit existing contracts
Important: Always audit your contracts before mainnet deployment. Consider using established libraries and getting professional audits for production code.