Blockchain Development with Solidity: Building Smart Contracts


Blockchain Development with Solidity: Building Smart Contracts

Blockchain technology and smart contracts are revolutionizing various industries. This guide will show you how to develop smart contracts using Solidity and build decentralized applications.

Getting Started with Solidity

First, let's set up our development environment:

# Install Truffle globally
npm install -g truffle

# Create a new project
mkdir my-dapp
cd my-dapp
truffle init

# Install dependencies
npm install @openzeppelin/contracts hardhat @nomiclabs/hardhat-ethers ethers

Basic Smart Contract

Let's create a simple token contract:

// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract MyToken is ERC20, Ownable {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
        _mint(msg.sender, initialSupply * 10 ** decimals());
    }
    
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
    
    function burn(uint256 amount) public {
        _burn(msg.sender, amount);
    }
}

Project: DeFi Lending Platform

Let's build a decentralized lending platform:

// contracts/LendingPool.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract LendingPool is ReentrancyGuard, Ownable {
    struct LendingPosition {
        uint256 amount;
        uint256 timestamp;
    }
    
    struct BorrowPosition {
        uint256 amount;
        uint256 collateral;
        uint256 timestamp;
    }
    
    IERC20 public token;
    mapping(address => LendingPosition) public lenders;
    mapping(address => BorrowPosition) public borrowers;
    
    uint256 public totalLent;
    uint256 public totalBorrowed;
    uint256 public interestRate = 5; // 5% APR
    uint256 public collateralRatio = 150; // 150% collateral required
    
    event Deposit(address indexed lender, uint256 amount);
    event Withdraw(address indexed lender, uint256 amount);
    event Borrow(address indexed borrower, uint256 amount, uint256 collateral);
    event Repay(address indexed borrower, uint256 amount);
    
    constructor(address _token) {
        token = IERC20(_token);
    }
    
    function deposit(uint256 amount) external nonReentrant {
        require(amount > 0, "Amount must be greater than 0");
        require(
            token.transferFrom(msg.sender, address(this), amount),
            "Transfer failed"
        );
        
        LendingPosition storage position = lenders[msg.sender];
        position.amount += amount;
        position.timestamp = block.timestamp;
        totalLent += amount;
        
        emit Deposit(msg.sender, amount);
    }
    
    function withdraw(uint256 amount) external nonReentrant {
        LendingPosition storage position = lenders[msg.sender];
        require(position.amount >= amount, "Insufficient balance");
        
        uint256 interest = calculateInterest(
            amount,
            position.timestamp,
            block.timestamp
        );
        uint256 totalAmount = amount + interest;
        
        require(
            totalAmount <= address(this).balance,
            "Insufficient pool balance"
        );
        
        position.amount -= amount;
        position.timestamp = block.timestamp;
        totalLent -= amount;
        
        require(
            token.transfer(msg.sender, totalAmount),
            "Transfer failed"
        );
        
        emit Withdraw(msg.sender, totalAmount);
    }
    
    function borrow(uint256 amount) external payable nonReentrant {
        require(amount > 0, "Amount must be greater than 0");
        require(
            msg.value >= (amount * collateralRatio) / 100,
            "Insufficient collateral"
        );
        
        BorrowPosition storage position = borrowers[msg.sender];
        require(position.amount == 0, "Existing loan must be repaid first");
        
        require(
            amount <= totalLent - totalBorrowed,
            "Insufficient liquidity"
        );
        
        position.amount = amount;
        position.collateral = msg.value;
        position.timestamp = block.timestamp;
        totalBorrowed += amount;
        
        require(
            token.transfer(msg.sender, amount),
            "Transfer failed"
        );
        
        emit Borrow(msg.sender, amount, msg.value);
    }
    
    function repay() external nonReentrant {
        BorrowPosition storage position = borrowers[msg.sender];
        require(position.amount > 0, "No loan to repay");
        
        uint256 interest = calculateInterest(
            position.amount,
            position.timestamp,
            block.timestamp
        );
        uint256 totalAmount = position.amount + interest;
        
        require(
            token.transferFrom(msg.sender, address(this), totalAmount),
            "Transfer failed"
        );
        
        payable(msg.sender).transfer(position.collateral);
        
        totalBorrowed -= position.amount;
        delete borrowers[msg.sender];
        
        emit Repay(msg.sender, totalAmount);
    }
    
    function calculateInterest(
        uint256 amount,
        uint256 fromTimestamp,
        uint256 toTimestamp
    ) public view returns (uint256) {
        uint256 timeElapsed = toTimestamp - fromTimestamp;
        return (amount * interestRate * timeElapsed) / (365 days * 100);
    }
    
    function setInterestRate(uint256 newRate) external onlyOwner {
        require(newRate > 0, "Interest rate must be greater than 0");
        interestRate = newRate;
    }
    
    function setCollateralRatio(uint256 newRatio) external onlyOwner {
        require(newRatio >= 100, "Collateral ratio must be at least 100%");
        collateralRatio = newRatio;
    }
}

NFT Marketplace

Let's create an NFT marketplace:

// contracts/NFTMarketplace.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFTMarketplace is ReentrancyGuard {
    using Counters for Counters.Counter;
    
    struct Listing {
        address seller;
        address nftContract;
        uint256 tokenId;
        uint256 price;
        bool active;
    }
    
    Counters.Counter private _listingIds;
    mapping(uint256 => Listing) public listings;
    uint256 public platformFee = 25; // 2.5%
    
    event Listed(
        uint256 indexed listingId,
        address indexed seller,
        address indexed nftContract,
        uint256 tokenId,
        uint256 price
    );
    
    event Sale(
        uint256 indexed listingId,
        address indexed buyer,
        address indexed nftContract,
        uint256 tokenId,
        uint256 price
    );
    
    event ListingCanceled(uint256 indexed listingId);
    
    function listNFT(
        address nftContract,
        uint256 tokenId,
        uint256 price
    ) external nonReentrant {
        require(price > 0, "Price must be greater than 0");
        
        IERC721 nft = IERC721(nftContract);
        require(
            nft.ownerOf(tokenId) == msg.sender,
            "Not the owner"
        );
        require(
            nft.getApproved(tokenId) == address(this),
            "NFT not approved"
        );
        
        _listingIds.increment();
        uint256 listingId = _listingIds.current();
        
        listings[listingId] = Listing({
            seller: msg.sender,
            nftContract: nftContract,
            tokenId: tokenId,
            price: price,
            active: true
        });
        
        emit Listed(
            listingId,
            msg.sender,
            nftContract,
            tokenId,
            price
        );
    }
    
    function buyNFT(uint256 listingId) external payable nonReentrant {
        Listing storage listing = listings[listingId];
        require(listing.active, "Listing not active");
        require(msg.value == listing.price, "Incorrect price");
        
        listing.active = false;
        
        IERC721 nft = IERC721(listing.nftContract);
        
        uint256 fee = (msg.value * platformFee) / 1000;
        uint256 sellerAmount = msg.value - fee;
        
        payable(listing.seller).transfer(sellerAmount);
        
        nft.safeTransferFrom(
            listing.seller,
            msg.sender,
            listing.tokenId
        );
        
        emit Sale(
            listingId,
            msg.sender,
            listing.nftContract,
            listing.tokenId,
            msg.value
        );
    }
    
    function cancelListing(uint256 listingId) external nonReentrant {
        Listing storage listing = listings[listingId];
        require(listing.active, "Listing not active");
        require(
            listing.seller == msg.sender,
            "Not the seller"
        );
        
        listing.active = false;
        
        emit ListingCanceled(listingId);
    }
}

Testing Smart Contracts

Let's write tests for our contracts:

// test/LendingPool.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("LendingPool", function () {
  let Token;
  let token;
  let LendingPool;
  let lendingPool;
  let owner;
  let addr1;
  let addr2;
  
  beforeEach(async function () {
    [owner, addr1, addr2] = await ethers.getSigners();
    
    Token = await ethers.getContractFactory("MyToken");
    token = await Token.deploy(1000000);
    await token.deployed();
    
    LendingPool = await ethers.getContractFactory("LendingPool");
    lendingPool = await LendingPool.deploy(token.address);
    await lendingPool.deployed();
    
    // Approve lending pool to spend tokens
    await token.approve(lendingPool.address, ethers.constants.MaxUint256);
    await token.connect(addr1).approve(
      lendingPool.address,
      ethers.constants.MaxUint256
    );
  });
  
  describe("Deposit", function () {
    it("Should allow deposits", async function () {
      const depositAmount = ethers.utils.parseEther("100");
      await token.transfer(addr1.address, depositAmount);
      
      await expect(
        lendingPool.connect(addr1).deposit(depositAmount)
      )
        .to.emit(lendingPool, "Deposit")
        .withArgs(addr1.address, depositAmount);
      
      const position = await lendingPool.lenders(addr1.address);
      expect(position.amount).to.equal(depositAmount);
    });
  });
  
  describe("Borrow", function () {
    it("Should allow borrowing with sufficient collateral", async function () {
      const depositAmount = ethers.utils.parseEther("100");
      const borrowAmount = ethers.utils.parseEther("50");
      
      await token.transfer(addr1.address, depositAmount);
      await lendingPool.connect(addr1).deposit(depositAmount);
      
      const collateralRequired = borrowAmount
        .mul(150)
        .div(100);
      
      await expect(
        lendingPool.connect(addr2).borrow(borrowAmount, {
          value: collateralRequired
        })
      )
        .to.emit(lendingPool, "Borrow")
        .withArgs(addr2.address, borrowAmount, collateralRequired);
      
      const position = await lendingPool.borrowers(addr2.address);
      expect(position.amount).to.equal(borrowAmount);
      expect(position.collateral).to.equal(collateralRequired);
    });
  });
});

Deployment

Deploy contracts using Hardhat:

// scripts/deploy.js
async function main() {
  const [deployer] = await ethers.getSigners();
  console.log("Deploying contracts with:", deployer.address);
  
  // Deploy token
  const Token = await ethers.getContractFactory("MyToken");
  const token = await Token.deploy(1000000);
  await token.deployed();
  console.log("Token deployed to:", token.address);
  
  // Deploy lending pool
  const LendingPool = await ethers.getContractFactory("LendingPool");
  const lendingPool = await LendingPool.deploy(token.address);
  await lendingPool.deployed();
  console.log("LendingPool deployed to:", lendingPool.address);
  
  // Deploy NFT marketplace
  const NFTMarketplace = await ethers.getContractFactory("NFTMarketplace");
  const marketplace = await NFTMarketplace.deploy();
  await marketplace.deployed();
  console.log("NFTMarketplace deployed to:", marketplace.address);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Best Practices

  1. Security

    • Use OpenZeppelin contracts
    • Implement reentrancy guards
    • Follow checks-effects-interactions pattern
    • Audit code thoroughly
  2. Gas Optimization

    • Minimize storage operations
    • Use events for off-chain data
    • Batch operations when possible
    • Optimize data types
  3. Testing

    • Write comprehensive tests
    • Test edge cases
    • Use test coverage tools
    • Simulate attacks
  4. Deployment

    • Use proper networks
    • Verify contracts
    • Document deployments
    • Monitor transactions

Common Patterns

  1. Access Control
contract AccessControl {
    mapping(address => bool) public admins;
    
    modifier onlyAdmin() {
        require(admins[msg.sender], "Not an admin");
        _;
    }
    
    function addAdmin(address admin) external onlyAdmin {
        admins[admin] = true;
    }
    
    function removeAdmin(address admin) external onlyAdmin {
        admins[admin] = false;
    }
}
  1. Pausable
contract Pausable {
    bool public paused;
    
    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }
    
    function pause() external onlyOwner {
        paused = true;
    }
    
    function unpause() external onlyOwner {
        paused = false;
    }
}

Conclusion

Solidity and blockchain development enable:

  • Decentralized applications
  • Trustless transactions
  • Automated agreements
  • Digital asset management

Keep learning and exploring the blockchain ecosystem to build innovative applications.


Further Reading