Triển khai smart contract trên Lisk sử dụng Hardhat
Lưu ý 1: Cài đặt Node và npm trước khi bắt đầu
Lưu ý 2: Wallet deploy contract cần có balance trên Lisk Testnet
Tạo project Hardhat
- Tạo folder làm việc
mkdir lisk_smart_contract
cd lisk_smart_contract
- Cài đặt node project
npm init --y
- Cài đặt thư viện hardhat
npm install --save-dev hardhat
- Tạo hardhat project
npx hardhat
Tuỳ chỉnh Hardhat với Lisk Sepolia
- Cài đặt dotenv
npm install --save-dev dotenv
- Tạo file .env
PRIVATE_KEY=<YOUR_PRIVATE_KEY>
- Tuỳ chỉnh
hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
require('dotenv').config();
const config: HardhatUserConfig = {
solidity: "0.8.23",
networks: {
// for testnet
'lisk-sepolia': {
url: 'https://rpc.sepolia-api.lisk.com',
accounts: [process.env.PRIVATE_KEY as string],
gasPrice: 1000000000,
},
},
};
export default config;
Tạo smart contract ERC20
- Cài đặt thư viện OpenZeppelin
npm install --save @openzeppelin/contracts
- Tạo file
contracts/ERC20.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor() ERC20("MyToken", "MT") {
_mint(msg.sender, 1000000 * (10 ** uint256(decimals())));
}
}
Compile smart contract ERC20
npx hardhat compile
Test smart contract ERC20
- Tạo file
test/ERC20.test
/// test/ERC20.ts
import { expect } from "chai";
import { ethers } from "hardhat";
describe("MyToken", function () {
async function deployTokenFixture() {
const [owner, otherAccount] = await ethers.getSigners();
const MyToken = await ethers.getContractFactory("MyToken");
const token = await MyToken.deploy();
const initialSupply = BigInt(1000000) * BigInt(10) ** BigInt(18); // 1 million tokens with 18 decimals
return { token, owner, otherAccount, initialSupply };
}
describe("Deployment", function () {
it("Should set the right name and symbol", async function () {
const { token } = await deployTokenFixture();
expect(await token.name()).to.equal("MyToken");
expect(await token.symbol()).to.equal("MT");
});
it("Should assign the total supply of tokens to the owner", async function () {
const { token, owner, initialSupply } = await deployTokenFixture();
expect(await token.totalSupply()).to.equal(initialSupply);
expect(await token.balanceOf(owner.address)).to.equal(initialSupply);
});
});
describe("Transfers", function () {
it("Should transfer tokens between accounts", async function () {
const { token, owner, otherAccount } = await deployTokenFixture();
const transferAmount = BigInt(100) * BigInt(10) ** BigInt(18); // 100 tokens
await token.transfer(otherAccount.address, transferAmount);
expect(await token.balanceOf(otherAccount.address)).to.equal(transferAmount);
expect(await token.balanceOf(owner.address)).to.equal(
(await token.totalSupply()) - transferAmount
);
});
it("Should fail if sender doesn't have enough tokens", async function () {
const { token, otherAccount } = await deployTokenFixture();
await expect(
token.connect(otherAccount).transfer(otherAccount.address, 1)
).to.be.revertedWithCustomError(token, "ERC20InsufficientBalance");
});
});
describe("Allowances", function () {
it("Should allow approved spender to transfer tokens", async function () {
const { token, owner, otherAccount } = await deployTokenFixture();
const transferAmount = BigInt(100) * BigInt(10) ** BigInt(18); // 100 tokens
await token.approve(otherAccount.address, transferAmount);
await token.connect(otherAccount).transferFrom(owner.address, otherAccount.address, transferAmount);
expect(await token.balanceOf(otherAccount.address)).to.equal(transferAmount);
});
it("Should fail if spender doesn't have enough allowance", async function () {
const { token, owner, otherAccount } = await deployTokenFixture();
const transferAmount = BigInt(100) * BigInt(10) ** BigInt(18); // 100 tokens
await token.approve(otherAccount.address, transferAmount - BigInt(1));
await expect(
token.connect(otherAccount).transferFrom(owner.address, otherAccount.address, transferAmount)
).to.be.revertedWithCustomError(token, "ERC20InsufficientAllowance");
});
});
});
- Chạy test
npx hardhat test
MyToken
Deployment
✔ Should set the right name and symbol (320ms)
✔ Should assign the total supply of tokens to the owner
Transfers
✔ Should transfer tokens between accounts
✔ Should fail if sender doesn't have enough tokens
Allowances
✔ Should allow approved spender to transfer tokens
✔ Should fail if spender doesn't have enough allowance
6 passing (373ms)
Deploy smart contract ERC20
- Tạo file
ignition/modules/ERC20.ts
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
const ERC20Module = buildModule("ERC20Module", (m) => {
const token = m.contract("MyToken", []);
return { token };
});
export default ERC20Module;
- Deploy và tương tác với smart contract ERC20
npx hardhat ignition deploy ignition/modules/ERC20.ts --network lisk-sepolia
- Kết quả
✔ Confirm deploy to network lisk-sepolia (4202)? … yes
Hardhat Ignition 🚀
Deploying [ ERC20Module ]
Batch #1
Executed ERC20Module#MyToken
[ ERC20Module ] successfully deployed 🚀
Deployed Addresses
ERC20Module#MyToken - 0x962c0940d72E7Db6c9a5F81f1cA87D8DB2B82A23
Tương tác write and read ERC20 contract
- Tạo file
scripts/transfer.ts
/// scripts/transfer.ts
import { ethers } from "hardhat";
async function main() {
// Thay đổi token address và recipient address
const tokenAddress = "0x962c0940d72E7Db6c9a5F81f1cA87D8DB2B82A23";
const recipientAddress = "0x783FC27915754512E72b5811599504eCa458E4C5";
const transferAmount = ethers.parseEther("100"); // 100 tokens
const token = await ethers.getContractAt("MyToken", tokenAddress);
const [signer] = await ethers.getSigners();
console.log("Transferring from:", signer.address);
const tx = await token.transfer(recipientAddress, transferAmount);
console.log("Transfer transaction hash:", tx.hash);
await tx.wait();
console.log("Transfer completed!");
const senderBalance = await token.balanceOf(signer.address);
const recipientBalance = await token.balanceOf(recipientAddress);
console.log("Sender balance:", ethers.formatEther(senderBalance));
console.log("Recipient balance:", ethers.formatEther(recipientBalance));
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
- Chạy script
npx hardhat run scripts/transfer.ts --network lisk-sepolia
- Kết quả
Transferring from: 0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac
Transfer transaction hash: 0x5ebf04c2a64c3e4c8a0e18ea6d97bcbcb01897629708a31923bec90e17c58406
Transfer completed!
Sender balance: 999900.0
Recipient balance: 100.0
- Kiểm tra transaction hash trên Lisk Testnet Block Explorer
https://sepolia-blockscout.lisk.com/tx/0x5ebf04c2a64c3e4c8a0e18ea6d97bcbcb01897629708a31923bec90e17c58406
Verify smart contract ERC20
- Tuỳ chỉnh thêm explorer vào
hardhat.config.ts
const config: HardhatUserConfig = {
....
etherscan: {
apiKey: {
"lisk-sepolia": "123"
},
customChains: [
{
network: "lisk-sepolia",
chainId: 4202,
urls: {
apiURL: "https://sepolia-blockscout.lisk.com/api",
browserURL: "https://sepolia-blockscout.lisk.com"
}
}
]
},
sourcify: {
enabled: false
},
};
- Câu lệnh verify smart contract
npx hardhat verify --network lisk-sepolia <contract_address> <constructor_arguments>
- Kết quả
npx hardhat verify --network lisk-sepolia 0x962c0940d72E7Db6c9a5F81f1cA87D8DB2B82A23
[WARNING] Network and explorer-specific api keys are deprecated in favour of the new Etherscan v2 api. Support for v1 is expected to end by May 31st, 2025. To migrate, please specify a single Etherscan.io api key the apiKey config value.
Successfully submitted source code for contract
contracts/ERC20.sol:MyToken at 0x962c0940d72E7Db6c9a5F81f1cA87D8DB2B82A23
for verification on the block explorer. Waiting for verification result...
Successfully verified contract MyToken on the block explorer.
https://sepolia-blockscout.lisk.com/address/0x962c0940d72E7Db6c9a5F81f1cA87D8DB2B82A23#code