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.