A Beginner's Guide to Develop Smart Contracts

In this blog, I will walk through creating, compiling, deploying, and testing a smart contract using Truffle and Ganache. We will use the InvoiceManager smart contract as our case study.

Prerequisite:

Development Tools:

  • Node.js and npm installed
  • Truffle: A development framework for Ethereum
  • Ganache: A personal Ethereum blockchain for development

Libraries and Frameworks:

  • OpenZeppelin: A library for secure smart contract development
  • Mocha: A JavaScript test framework
  • Chai: An assertion library for Node.js

Basic Knowledge:

  • Basic understanding of Solidity and smart contracts

What is Truffle?

Truffle is a development framework for Ethereum that provides a suite of tools to simplify developing, testing, and deploying smart contracts. Truffle makes managing projects, compiling contracts, migrating them to different networks, and running tests easier.

What is Ganache?

Ganache is a personal Ethereum blockchain used for development. It allows you to create a local blockchain instance to deploy contracts, develop applications, and run tests. Ganache provides a user-friendly interface and detailed logs of all transactions and events, making it an essential tool for blockchain developers.

What is OpenZeppelin?

OpenZeppelin is a library for secure smart contract development. It provides tested and audited code that you can use to build your decentralized applications. By using OpenZeppelin, you can avoid common security pitfalls and leverage well-known best practices in the Ethereum community.

What are Mocha and Chai?

Mocha is a JavaScript test framework that runs on Node.js, providing a robust foundation for writing asynchronous tests with flexibility and simplicity. Chai is an assertion library for Node.js that pairs well with Mocha, offering a variety of assertions for more readable and expressive tests.

Step 1: Setting Up the Environment

Installing Truffle and Ganache

First, ensure you have Truffle and Ganache installed. If not, install them using npm:

npm install -g truffle
npm install -g ganache-cli

Step 2: Creating the Truffle Project

Initializing the Truffle Project

Create a new directory for your project and initialize a Truffle project:

mkdir InvoiceManagerProject
cd InvoiceManagerProject
truffle init

This creates the basic structure for a Truffle project.

Step 3: Writing the Smart Contract

Creating the InvoiceManager Contract

In the contracts directory, create a new file named InvoiceManager.sol and add the following code:

pragma solidity ^0.8.20;

import "@openzeppelin/contracts/access/AccessControl.sol";

contract InvoiceManager is AccessControl {
    bytes32 public constant ISSUER_ROLE = keccak256("ISSUER_ROLE");
    bytes32 public constant PAYER_ROLE = keccak256("PAYER_ROLE");

    struct Invoice {
        uint256 id;
        string description;
        uint256 amount;
        uint256 dueDate;
        uint256 lateFee;
        uint256 remainingAmount;
        address issuer;
        address payer;
        bool verified;
        bool paid;
    }

    struct Loan {
        uint256 id;
        uint256 invoiceId;
        uint256 principal;
        uint256 interestRate; // Interest rate in basis points (e.g., 250 for 2.5%)
        uint256 term; // Term in days
        uint256 startDate; // Timestamp of the start date
        address[] institutionalLenders;
        address[] individualLenders;
        mapping(address => uint256) contributions;
        address borrower;
        bool repaid;
        bool defaulted;
    }

    struct AuditRecord {
        uint256 timestamp;
        string action;
        address performedBy;
        uint256 invoiceId;
        uint256 amount;
    }

    mapping(uint256 => Invoice) public invoices;
    mapping(uint256 => Loan) public loans;
    mapping(uint256 => AuditRecord[]) public auditTrail;

    uint256 public nextInvoiceId;
    uint256 public nextLoanId;
    uint256 constant BASIS_POINTS = 10000;

    event InvoiceCreated(uint256 id, string description, uint256 amount, uint256 dueDate, uint256 lateFee, address indexed issuer, address indexed payer);
    event LoanRepaid(uint256 loanId, address indexed borrower);
    event AuditTrailUpdated(uint256 timestamp, string action, address indexed performedBy, uint256 invoiceId, uint256 amount);

    constructor() {
        _setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }

    function createInvoice(string memory description, uint256 amount, uint256 dueDate, uint256 lateFee, address payer) public onlyRole(ISSUER_ROLE) returns (uint256) {
        uint256 invoiceId = nextInvoiceId++;
        invoices[invoiceId] = Invoice(invoiceId, description, amount, dueDate, lateFee, amount, msg.sender, payer, false, false);

        emit InvoiceCreated(invoiceId, description, amount, dueDate, lateFee, msg.sender, payer);
        updateAuditTrail("Invoice Created", msg.sender, invoiceId, amount);
        return invoiceId;
    }

    function repayEarly(uint256 loanId) public payable {
        Loan storage loan = loans[loanId];
        require(!loan.repaid, "Loan already repaid");
        require(msg.value >= loan.principal, "Payment less than principal");

        uint256 remainingInterest = calculateRemainingInterest(loanId);
        uint256 repaymentAmount = loan.principal + remainingInterest;

        require(msg.value >= repaymentAmount, "Insufficient amount to repay loan early");

        for (uint256 i = 0; i < loan.institutionalLenders.length; i++) {
            uint256 lenderShare = (loan.contributions[loan.institutionalLenders[i]] * repaymentAmount) / loan.principal;
            payable(loan.institutionalLenders[i]).transfer(lenderShare);
        }

        for (uint256 i = 0; i < loan.individualLenders.length; i++) {
            uint256 lenderShare = (loan.contributions[loan.individualLenders[i]] * repaymentAmount) / loan.principal;
            payable(loan.individualLenders[i]).transfer(lenderShare);
        }

        loan.repaid = true;
        emit LoanRepaid(loanId, loan.borrower);
        updateAuditTrail("Early Repayment", msg.sender, loanId, repaymentAmount);
    }

    function calculateRemainingInterest(uint256 loanId) public view returns (uint256) {
        Loan storage loan = loans[loanId];
        uint256 elapsedTime = block.timestamp - loan.startDate;
        uint256 interestPerDay = (loan.principal * loan.interestRate) / BASIS_POINTS / 365;
        uint256 totalInterest = interestPerDay * loan.term;
        uint256 paidInterest = interestPerDay * (elapsedTime / 1 days);

        return totalInterest - paidInterest;
    }

    function getLoanDetails(uint256 loanId) public view returns (
        uint256 id,
        uint256 invoiceId,
        uint256 principal,
        uint256 interestRate,
        uint256 term,
        uint256 startDate,
        address[] memory institutionalLenders,
        address[] memory individualLenders,
        address borrower,
        bool repaid,
        bool defaulted
    ) {
        Loan storage loan = loans[loanId];
        return (
            loan.id,
            loan.invoiceId,
            loan.principal,
            loan.interestRate,
            loan.term,
            loan.startDate,
            loan.institutionalLenders,
            loan.individualLenders,
            loan.borrower,
            loan.repaid,
            loan.defaulted
        );
    }

    function updateAuditTrail(string memory action, address performedBy, uint256 invoiceId, uint256 amount) internal {
        AuditRecord memory record = AuditRecord(block.timestamp, action, performedBy, invoiceId, amount);
        auditTrail[invoiceId].push(record);
        emit AuditTrailUpdated(block.timestamp, action, performedBy, invoiceId, amount);
    }
}

Adding OpenZeppelin Dependency

Install OpenZeppelin contracts:

npm install @openzeppelin/contracts

Configuring Truffle

Update the truffle-config.js file to include the Solidity compiler version and network settings for Ganache:

module.exports = {
    networks: {
        development: {
            host: "127.0.0.1",     // Localhost (default: none)
            port: 8545,            // Standard Ethereum port (default: none)
            network_id: "*",       // Any network (default: none)
        },
    },

    compilers: {
        solc: {
            version: "0.8.20",    // Updated to match OpenZeppelin's latest version
            settings: {          // See the solidity docs for advice about optimization and evmVersion
                optimizer: {
                    enabled: true,
                    runs: 200
                },
                evmVersion: "istanbul"
            }
        }
    },
};

Step 4: Compiling the Smart Contract

Compiling the Contract

Compile your smart contract with the following command:

truffle compile

This command will compile your InvoiceManager.sol contract and generate the necessary artefacts in the build/contracts directory.

Step 5: Deploying the Smart Contract

Setting Up Ganache

Run Ganache to create a local Ethereum blockchain for development:

ganache-cli

Configuring Deployment Script

Create a deployment script in the migrations directory, e.g., 2_deploy_contracts.js:

const InvoiceManager = artifacts.require("InvoiceManager");

module.exports = function (deployer) {
    deployer.deploy(InvoiceManager);
};

Deploying to Ganache

Deploy the smart contract to the local Ganache blockchain:

truffle migrate

Step 6: Writing Unit Tests

Using Mocha and Chai for Unit Testing

What are Mocha and Chai?

Mocha is a JavaScript test framework that runs on Node.js. It provides a robust foundation for writing asynchronous tests with flexibility and simplicity. Chai is an assertion library for Node.js that complements Mocha. It offers a variety of assertions for more readable and expressive tests.

Writing Unit Tests

Create a unit test file in the test directory, e.g., test_invoice_manager.js, and write the unit tests:

const InvoiceManager = artifacts.require("InvoiceManager");

contract("InvoiceManager", accounts => {
    const [admin, issuer, payer, institutionalLender, individualLender] = accounts;

    beforeEach(async () => {
        this.invoiceManagerInstance = await InvoiceManager.new({ from: admin });

        // Grant roles to issuer and payer
        await this.invoiceManagerInstance.grantRole(await this.invoiceManagerInstance.ISSUER_ROLE(), issuer, { from: admin });
        await this.invoiceManagerInstance.grantRole(await this.invoiceManagerInstance.PAYER_ROLE(), payer, { from: admin });

        // Set up a loan for testing
        const description = "Test Invoice";
        const amount = 1000;
        const dueDate = Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60); // 30 days from now
        const lateFee = 100;

        // Create invoice
        const invoiceResult = await this.invoiceManagerInstance.createInvoice(description, amount, dueDate, lateFee, payer, { from: issuer });
        const invoiceId = invoiceResult.logs[0].args.id.toNumber();

        // Manually set up loan details
        await this.invoiceManagerInstance.loans(invoiceId).then(loan => {
            loan.id = invoiceId;
            loan.invoiceId = invoiceId;
            loan.principal = amount;
            loan.interestRate = 250; // 2.5%
            loan.term = 30; // 30 days
            loan.startDate = Math.floor(Date.now() / 1000);
            loan.institutionalLenders = [institutionalLender];
            loan.individualLenders = [individualLender];
            loan.contributions[institutionalLender] = 500;
            loan.contributions[individualLender] = 500;
            loan.borrower = payer;
            loan.repaid = false;
            loan.defaulted = false;
        });
    });

    it("should create an invoice successfully", async () => {
        const description = "Test Invoice";
        const amount = 1000;
        const dueDate = Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60); // 30 days from now
        const lateFee = 100;

        // Call createInvoice method
        const result = await this.invoiceManagerInstance.createInvoice(description, amount, dueDate, lateFee, payer, { from: issuer });

        // Get the created invoice ID from the emitted event
        const invoiceId = result.logs[0].args.id.toNumber();

        // Retrieve the created invoice from the contract
        const invoice = await this.invoiceManagerInstance.invoices(invoiceId);

        assert.equal(invoice.id.toNumber(), invoiceId, "Invoice ID does not match");
        assert.equal(invoice.description, description, "Invoice description does not match");
        assert.equal(invoice.amount.toNumber(), amount, "Invoice amount does not match");
        assert.equal(invoice.dueDate.toNumber(), dueDate, "Invoice due date does not match");
        assert.equal(invoice.lateFee.toNumber(), lateFee, "Invoice late fee does not match");
        assert.equal(invoice.issuer, issuer, "Invoice issuer does not match");
        assert.equal(invoice.payer, payer, "Invoice payer does not match");
        assert.equal(invoice.verified, false, "Invoice should not be verified");
        assert.equal(invoice.paid, false, "Invoice should not be paid");
    });

    it("should repay the loan early", async () => {
        const loanId = await this.invoiceManagerInstance.nextLoanId() - 1;
        const loan = await this.invoiceManagerInstance.loans(loanId);

        // Calculate the repayment amount
        const remainingInterest = await this.invoiceManagerInstance.calculateRemainingInterest(loanId);
        const repaymentAmount = loan.principal + remainingInterest;

        // Repay the loan
        await this.invoiceManagerInstance.repayEarly(loanId, { from: payer, value: repaymentAmount });

        // Check loan status
        const updatedLoan = await this.invoiceManagerInstance.loans(loanId);
        assert.equal(updatedLoan.repaid, true, "Loan was not marked as repaid");

        // Verify that lenders received their shares
        const institutionalLenderBalance = await web3.eth.getBalance(institutionalLender);
        const individualLenderBalance = await web3.eth.getBalance(individualLender);
        assert(institutionalLenderBalance.toNumber() > 0, "Institutional lender did not receive their share");
        assert(individualLenderBalance.toNumber() > 0, "Individual lender did not receive their share");
    });
});

Running the Tests

Run the tests with the following command:

truffle test

This command will execute your unit tests and provide feedback on their success or failure.

Conclusion

In this blog, we explored how to create, compile, deploy, and test a smart contract using Truffle and Ganache. By following these steps, you can build robust and reliable smart contracts for the Ethereum blockchain. The InvoiceManager smart contract served as our case study, demonstrating key functionalities such as creating invoices and repaying loans early.

Key Takeaways:

  • Truffle: A powerful framework for developing, testing, and deploying smart contracts.
  • Ganache: A local blockchain for development and testing purposes.
  • OpenZeppelin: A library of reusable smart contract components.
  • Mocha and Chai: Essential tools for writing and running unit tests in JavaScript, ensuring your smart contracts are functioning correctly.

Leveraging these tools can streamline the development process and ensure the security and efficiency of your smart contracts.