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

  1. Node.js & npm: JavaScript runtime
  2. Hardhat: Development framework
  3. MetaMask: Wallet for testing
  4. VS Code: Code editor
  5. 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

  1. 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);
    }
}
  1. 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+
    }
}
  1. 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

  1. Build a simple DeFi protocol
  2. Create an NFT collection
  3. Implement a DAO voting system
  4. Study advanced patterns
  5. Audit existing contracts

Important: Always audit your contracts before mainnet deployment. Consider using established libraries and getting professional audits for production code.