이번 장에서는 Hardhat을 사용하여 Klaytn Baobab 네트워크에 Soulbound 토큰을 배포하는 과정을 안내해 드리겠습니다.
Hardhat은 다음을 도와줄 스마트 컨트랙트 개발 환경입니다:
스마트 컨트랙트를 개발하고 컴파일합니다.
스마트 컨트랙트와 dApps를 디버그, 테스트, 배포합니다.
Soul-bound 토큰(SBTs) 은 전송할 수 없는 NFTs(비대체 가능 토큰) 입니다. 즉, 한 번 획득하면 다른 사용자에게 팔거나 전송할 수 없습니다. SBT에 대해 더 자세히 알고 싶으시면, 어떻게 작동하는지 및 사용 사례에 대해 Vitalik Buterin이 발표한 이 참고 문서를 확인하실 수 있습니다.
이번 장에서는 커뮤니티에서 검증된 코드의 견고한 기반 위에 구축된 안전한 스마트 컨트랙트 개발을 위한 라이브러리인 Klaytn 컨트랙트를 사용하게 됩니다. Klaytn 컨트랙트는 오픈 제플린 컨트랙트으로부터 포크한 라이브러이입니다.
참고: 개발 환경 설정 장의 3 단계에서 이미 이 라이브러리를 설치했습니다.
1 단계: 탐색기 창에서 contracts 폴더를 선택하고, 새 파일 버튼을 클릭하여 SBT.sol이라는 새 파일을 생성합니다.
2 단계: 파일을 열고 아래의 코드를 붙여넣습니다:
// SPDX-License-Identifier: MITpragma solidity ^0.8.7;import"@klaytn/contracts/KIP/token/KIP17/KIP17.sol";import"@klaytn/contracts/utils/Counters.sol";import"@klaytn/contracts/access/Ownable.sol";contract SoulBoundToken is KIP17, Ownable {usingCounters for Counters.Counter;Counters.Counter private _tokenIdCounter;constructor() KIP17("SoulBoundToken","SBT") {}functionsafeMint(address to) publiconlyOwner { uint256 tokenId =_tokenIdCounter.current();_tokenIdCounter.increment();_safeMint(to, tokenId); }function_beforeTokenTransfer(address from, address to, uint256) pureoverrideinternal {require(from ==address(0) || to ==address(0),"This a Soulbound token. It cannot be transferred."); }function_burn(uint256 tokenId) internaloverride(KIP17) {super._burn(tokenId); }}
Code Walkthrough
This is your smart contract. line 1 shows that Hardhat uses the Solidity version 0.8.7 or greater. Other than that, it imports KIP17.sol and other supporting contracts. From lines 6-12, a smart contract that inherits KIP17 is been created. Also, the token name and symbol was passed in the constructor.
As you can see in the code above, the token name and symbol have been set to SoulBoundToken and SBT respectively. You can change the token name and symbol to anything you desire.
One major thing in this contract is that it prohibits token transfer, which makes the issued tokens soulbond.
Testing SBT Smart Contract
In this section, we would be testing some of our contract functionalities.
Step 1: In the Explorer pane, select the test folder and click the New File button to create a new file named sbtTest.ts
Step 2: Copy the code below in the sbtTest.ts file.
// This is an example test file. Hardhat will run every *.ts file in `test/`,// so feel free to add new ones.// Hardhat tests are normally written with Mocha and Chai.// We import Chai to use its asserting functions here.const { expect } =require("chai");// We use `loadFixture` to share common setups (or fixtures) between tests.// Using this simplifies your tests and makes them run faster, by taking// advantage of Hardhat Network's snapshot functionality.const { loadFixture } =require("@nomicfoundation/hardhat-network-helpers");// `describe` is a Mocha function that allows you to organize your tests.// Having your tests organized makes debugging them easier. All Mocha// functions are available in the global scope.//// `describe` receives the name of a section of your test suite, and a// callback. The callback must define the tests of that section. This callback// can't be an async function.describe("Token contract",function () {// We define a fixture to reuse the same setup in every test. We use// loadFixture to run this setup once, snapshot that state, and reset Hardhat// Network to that snapshot in every test.asyncfunctiondeployTokenFixture() {// Get the ContractFactory and Signers here.constsbt=awaitethers.getContractFactory("SoulBoundToken");const [owner,addr1,addr2] =awaitethers.getSigners();// To deploy our contract, we just have to call Token.deploy() and await// its deployed() method, which happens onces its transaction has been// mined.constsbtContract=awaitsbt.deploy();awaitsbtContract.deployed();// Fixtures can return anything you consider useful for your testsreturn { sbtContract, owner, addr1, addr2 }; }// You can nest describe calls to create subsections.describe("Deployment",function () {// `it` is another Mocha function. This is the one you use to define each// of your tests. It receives the test name, and a callback function.//// If the callback function is async, Mocha will `await` it.it("Should mint SBT to owner",asyncfunction () {const { sbtContract,owner } =awaitloadFixture(deployTokenFixture);constsafemint=awaitsbtContract.safeMint(owner.address);expect(awaitsbtContract.ownerOf(0)).to.equal(owner.address); }); });describe("Transactions",function () {it("Should prohibit token transfer using transferFrom",asyncfunction () {const { sbtContract,owner,addr1 } =awaitloadFixture( deployTokenFixture );constsafemintTx=awaitsbtContract.safeMint(owner.address);// prohibit token transfer of token id (0) from owner to addr1awaitexpect(sbtContract.transferFrom(owner.address,addr1.address,0) ).to.be.reverted; });it("Should prohibit token transfer using safeTransferFrom",asyncfunction () {const { sbtContract,owner,addr1 } =awaitloadFixture( deployTokenFixture );constsafemintTx=awaitsbtContract.safeMint(owner.address);// prohibit token transfer of token id (0) from owner to addr1awaitexpect(sbtContract['safeTransferFrom(address,address,uint256)'](owner.address,addr1.address,0 )).to.be.reverted;});});})
In the code you just copied, line 7 & 12 shows you imported expect from Chai and loadFixture from hardhat-network-helpers.
The tests above check the following:
Is the owner of a particular token id the same as who it was minted to?
Did it prohibit transfer of tokens between accounts?
Step 3: To run your test, run the command below:
npxhardhattesttest/sbtTest.ts
For more in-depth guide on testing, please check Hardhat testing.
Deploying the smart contract
Scripts are JavaScript/Typescript files that help you deploy contracts to the blockchain network. In this section, you will create a script for the smart contract.
Step 1: In the Explorer pane, select the “scripts” folder and click the New File button to create a new file named sbtDeploy.ts.
Step 2: Copy and paste the following code inside the file.
Note: input your MetaMask wallet address in the deployerAddr variable.
import { ethers } from"hardhat";asyncfunctionmain() {constdeployerAddr="Your Metamask wallet address";constdeployer=awaitethers.getSigner(deployerAddr);console.log(`Deploying contracts with the account: ${deployer.address}`);console.log(`Account balance: ${(awaitdeployer.getBalance()).toString()}`);constsbt=awaitethers.getContractFactory("SoulBoundToken");constsbtContract=awaitsbt.deploy();awaitsbtContract.deployed();console.log(`Congratulations! You have just successfully deployed your soul bound tokens.`);console.log(`SBT contract address is ${sbtContract.address}. You can verify on https://baobab.scope.klaytn.com/account/${sbtContract.address}`);
}// We recommend this pattern to be able to use async/await everywhere// and properly handle errors.main().catch((error) => {console.error(error);process.exitCode =1;});
Step 3: In the terminal, run the following command which tells Hardhat to deploy your SBT token on the Klaytn Test Network (Baobab)
npxhardhatrunscripts/sbtDeploy.ts--networkbaobab
Step 4: Open Klaytnscope to check if the SBT token has been deployed successfully.
Step 5: Copy and paste the deployed contract address in the search field and press Enter. You should see the recently deployed contract.
Hardhat Forking
Hardhat provides developers the functionality of simulating the mainnet (at any given block) to a local development network. One of the major benefit of this feature is that it enables developers to interact with deployed contract and also write test for complex cases.
For this feature to work effectively, you need to connect to an archive node. You can read more about this feature here
Forking Mainnet
Now that we have our Hardhat project set up let’s fork the Klaytn Mainnet using Hardhat. Open your terminal and run this command
After successfully running this command, your terminal looks like the above image. You'll have 20 development accounts that are pre-funded with 10,000 test tokens.
The forked chain's RPC server is listening at http://127.0.0.1:8545/. You can verify the forked network by querying the latest block number. Let's try to make a cURL to the RPC to get the block number. Open a new terminal window and use the following command:
curl --data '{"method":"eth_blockNumber","params":[],"id":1,"jsonrpc":"2.0"}' -H "Content-Type: application/json" -X POST localhost:8545
Output
The output is an hexadecimal as seen above. To get the block number from the hex, convert the hex to a decimal using this tool. You should get the latest block number from the time you forked the network. You can confirm the block number on klaytnscope.
Forking at a Block
With hardhat, you can fork the mainnet at a particular block. In that case, let’s fork the chain at block number 105701850.