Solidity is a programming language used to write smart contracts on blockchain platforms like Ethereum and Celo. Writing and deploying a Solidity smart contract can be a challenging task, especially for beginners. Debugging Solidity smart contracts is also a crucial task as it can prevent potential vulnerabilities and errors in the contract.
Hardhat and Tenderly are two popular tools that help developers in the development, deployment, and monitoring of smart contracts. Hardhat is an open-source development environment that provides a comprehensive set of tools for building, testing, and deploying smart contracts on Celo. Tenderly, on the other hand, is a blockchain monitoring and debugging platform that allows developers to track the performance of their smart contracts and identify any issues.
In this tutorial, I will walk you through the process of deploying, debugging, and monitoring a Solidity smart contract on Celo using Hardhat and Tenderly. By the end of this tutorial, you will have a good understanding of the development process and best practices for building and deploying Solidity smart contracts on Celo.
- Basic knowledge of Solidity
- Basic understanding of Celo, and
- JavaScript programming skills.
- Node.js: Install the latest version of Node.js from the official website.
- NPM: NPM is the package manager for Node.js. You can install it by running the command
npm install npm@latest -g
in your terminal. - Hardhat: Install Hardhat globally by running the command
npm install -g hardhat
in your terminal. - Celo Extension Wallet: Install the Celo Extension Wallet from the Chrome Web Store.
- Tenderly: Sign up for Tenderly here.
The Celo blockchain is a decentralized, open-source blockchain platform that enables users to build and run decentralized applications (dApps) and smart contracts. It is specifically designed to facilitate the development of mobile-first applications that can be used by people without access to traditional financial services.
Celo aims to create a more inclusive financial system by providing a decentralized platform for sending and receiving payments, issuing stablecoins, and building other financial applications. The platform is built on a Proof of Stake (PoS) consensus algorithm, which allows for more efficient transaction processing and lower fees compared to traditional Proof of Work (PoW) blockchains.
One of the key features of the Celo blockchain is its focus on mobile-first design, which means that the platform is optimized for use on mobile devices and is accessible to users with low-end smartphones and limited internet connectivity. This makes it a powerful tool for promoting financial inclusion and providing access to financial services to people in developing countries and other underserved communities.
It is safe to say that the Celo blockchain represents an exciting new frontier in the world of decentralized finance (DeFi), and its focus on mobile-first design and financial inclusion make it a unique and valuable addition to the blockchain ecosystem.
Now, you will set up the environment for building, testing, and deploying your smart contract on Celo using Hardhat.
- Create a new directory for your project and navigate into it.
mkdir my-project
cd my-project
- Initialize the project with NPM by running the command npm init -y.
npm init -y
- Install the required packages by running the following command.
npm install --save-dev hardhat @nomiclabs/hardhat-waffle \
@nomiclabs/hardhat-celo @celo-tools/celo-testkit
- Create a Hardhat configuration file by running the command
npx hardhat
.
npx hardhat
- Select "Create a sample project" and choose the default configuration.
What do you want to do? » Create a sample project
Select a starter kit » Empty
Do you want to add a .gitignore? » Yes
Successfully created Hardhat project my-project
- Open the
hardhat.config.js
file in your favorite code editor and add the following code.
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-celo");
require("@celo-tools/celo-testkit");
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
solidity: "0.8.0",
networks: {
hardhat: {},
celo: {
url: "https://forno.celo.org",
chainId: 42220,
gasPrice: 1000000000,
accounts: {
mnemonic: "your-mnemonic"
}
}
},
mocha: {
timeout: 20000
}
};
Here, you are importing the necessary plugins for Hardhat, including @nomiclabs/hardhat-waffle
, @nomiclabs/hardhat-celo
, and @celo-tools/celo-testkit
. You will then define the Solidity version we will be using and specify two networks: hardhat
and celo
.
The hardhat
network is the default network used for local development and testing. The celo
network is used for deploying the contract to the Celo network. We have specified the url
, chainId
, gasPrice
, and accounts
for the celo
network.
Note that you will need to replace the mnemonic with your own mnemonic.
In this section, you will write a simple Solidity smart contract that you will be deploying and testing in the following sections. Your smart contract will be a basic ERC20 token contract with the following functionality:
- Minting new tokens
- Transferring tokens between accounts
- Retrieving the balance of an account
Create a new file called Token.sol
in the contracts
directory with the following code.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Token is ERC20 {
constructor() ERC20("MyToken", "MTK") {
_mint(msg.sender, 1000000 * 10 ** decimals());
}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
function balanceOf(address account) public view override returns (uint256) {
return super.balanceOf(account);
}
}
Let me walk you through the code line by line:
import "@openzeppelin/contracts/token/ERC20/ERC20.sol
This is an import statement that imports the ERC20
contract from the @openzeppelin/contracts/token/ERC20
package. The ERC20
contract is a standard implementation of the ERC-20 token standard.
contract Token is ERC20
This is a contract declaration statement. It declares a new contract named Token
that extends the ERC20
contract.
constructor() ERC20("MyToken", "MTK")
This is a constructor function that is called when the contract is deployed. It calls the constructor of the ERC20
contract, passing in the token name "MyToken" and symbol "MTK". The _mint
function is then called to mint 1,000,000 tokens and assign them to the contract deployer.
_mint(msg.sender, 1000000 * 10 ** decimals())
This is an internal function that mints a specified amount of tokens and assigns them to a specified address. In this case, it is being called to mint 1,000,000 tokens and assign them to the msg.sender
, which is the address that deployed the contract.
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
function balanceOf(address account) public view override returns (uint256) {
return super.balanceOf(account);
}
These are three function declarations that define additional behavior for the Token
contract:
mint
: This function can be called by anyone and can be used to mint new tokens and assign them to a specified address.balanceOf
: This function returns the token balance of a specified address. It overrides the implementation of thebalanceOf
function in theERC20
contract, but simply callssuper.balanceOf
to retrieve the balance. This is an example of function overriding in Solidity.
You are importing the ERC20 contract from the OpenZeppelin library, which provides a standard implementation of the ERC20 token interface.
In this section, you will write tests to ensure that the Token contract works as intended.
- Create a new file called token.test.js in the test directory with the following code.
const { expect } = require("chai");
describe("Token contract", function () {
let Token;
let token;
let owner;
let addr1;
let addr2;
const initialSupply = ethers.utils.parseUnits("1000000", 18);
beforeEach(async function () {
[owner, addr1, addr2] = await ethers.getSigners();
Token = await ethers.getContractFactory("Token");
token = await Token.connect(owner).deploy();
await token.deployed();
});
describe("Deployment", function () {
it("Should set the right owner", async function () {
expect(await token.owner()).to.equal(owner.address);
});
it("Should assign the total supply of tokens to the owner", async function () {
const ownerBalance = await token.balanceOf(owner.address);
expect(ownerBalance).to.equal(initialSupply);
});
});
describe("Minting", function () {
it("Should mint new tokens", async function () {
const mintAmount = ethers.utils.parseUnits("1000", 18);
await token.connect(owner).mint(addr1.address, mintAmount);
const addr1Balance = await token.balanceOf(addr1.address);
expect(addr1Balance).to.equal(mintAmount);
});
});
describe("Transferring", function () {
it("Should transfer tokens between accounts", async function () {
const transferAmount = ethers.utils.parseUnits("100", 18);
await token.connect(owner).transfer(addr1.address, transferAmount);
const addr1Balance = await token.balanceOf(addr1.address);
expect(addr1Balance).to.equal(transferAmount);
await token.connect(addr1).transfer(addr2.address, transferAmount);
const addr2Balance = await token.balanceOf(addr2.address);
expect(addr2Balance).to.equal(transferAmount);
});
});
});
In the code above, you are using chai
to write tests for our Token
contract. You first defined some variables, including the contract owner owner
, addr1
, and addr2
, and the initial supply of tokens (initialSupply
).
In the beforeEach
function, you retrieve the contract factory and deploy a new instance of the contract using the owner's account.
You then have three describe
blocks: one for testing the deployment, one for testing minting new tokens, and one for testing transferring tokens between accounts.
In the first it
block, you are testing that the owner is set correctly when the contract is deployed.
In the second it
block, you test that the total supply of tokens is assigned to the owner when the contract is deployed.
In the third it block, you test that new tokens can be minted and assigned to an account.
In the fourth it block, wyoue test that tokens can be transferred between accounts.
- Run the tests by running the following command in the terminal.
npx hardhat test
This will run the tests you just wrote and output the results to the console.
-
Create a new account on Tenderly here.
-
Create a new project on Tenderly.
-
Edit the hardhat.config.js file and add the following code:
require('@nomiclabs/hardhat-celo')
const { PRIVATE_KEY } = process.env
const { TENDERLY_PROJECT_ID, TENDERLY_USERNAME, TENDERLY_API_KEY } = process.env
require('@tenderly/hardhat-tenderly')({
username: TENDERLY_USERNAME,
project: TENDERLY_PROJECT_ID,
apiKey: TENDERLY_API_KEY
})
module.exports = {
networks: {
celo: {
url: 'https://forno.celo.org',
accounts: [PRIVATE_KEY]
}
},
solidity: {
version: '0.8.4',
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
}
This configuration file now includes the Tenderly integration. You will need to add your Tenderly project ID, username, and API key as environment variables. 4. Initialize the Tenderly CLI and link it to your project.
npm install -g @tenderly/cli
Next, navigate to your project directory and log in to Tenderly:
tenderly login
Follow the prompts to log in with your Tenderly account.
Then, create a new project on Tenderly and copy the project ID from the URL. Set the TENDERLY_PROJECT_ID
environment variable to this ID:
export TENDERLY_PROJECT_ID=<your project ID>
Finally, link your Hardhat project to your Tenderly project using the Tenderly CLI:
npx hardhat tenderly link <your username>/<your project name>
This will create a new fork of the Celo blockchain and deploy your contract to it. You can now use Tenderly to debug your contract on this forked chain.
Next, you will deploy the Token smart contract to the Celo network using Hardhat.
- Compile the contract by running the following command.
npx hardhat compile
- Create a new deployment script called
deploy.js
in thescripts
directory with the following code.
const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
const Token = await ethers.getContractFactory("Token");
const token = await Token.deploy();
// The contract is NOT deployed yet; we must wait until it is mined
await token.deployed();
console.log("Token deployed to:", token.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Here, you are using ethers
to deploy the Token
contract to the celo
network. You first retrieve the deployer's account, and then use ethers.getContractFactory
to get the contract factory for Token
.
You then deploy the contract using token.deploy()
, and output the transaction hash to the console. You must wait for the transaction to be mined and the contract to be deployed before you can interact with it, so you use token.deployed()
to wait for this to happen.
- Deploy the contract to the Celo network by running the following command.
npx hardhat run scripts/deploy.js --network celo
This will deploy the Token
contract to the Celo network using the account specified in the hardhat.config.js
file.
Solidity smart contracts are a powerful tool for building decentralized applications on the Celo blockchain. However, like any software development process, building and deploying smart contracts can be challenging and require careful testing and debugging to ensure their correct functionality and security.
In this tutorial, we have covered the basics of Solidity smart contract development, using Hardhat and Tenderly to deploy and debug a sample ERC-20 token contract. We have walked through the step-by-step process of setting up a local development environment, compiling and deploying the contract, and debugging it using Tenderly.
We have also covered best practices for Solidity smart contract development, such as testing contracts using automated test scripts and auditing code for security vulnerabilities.
By following the steps outlined in this tutorial and continuing to learn and explore the Solidity language and the Celo blockchain, you can create powerful, secure, and reliable decentralized applications that can benefit users around the world.