Hardhat 사용해 스마트 컨트랙트 배포하기!

Boom Labs는 Web 3 개발자들을 온보딩시키고, 생태계를 활성화시키기 위해 교육을 비롯한 여러 활동들을 하고 있습니다. 저희 비전에 공감하여 동참하고자 하는 분들은 Boom Labs의 문을 두드려주세요!

Disclaimer: 이 글은 정보 전달을 위한 목적으로 작성되었으며, 특정 프로젝트에 대한 투자 권고, 법률적 자문 등 목적으로 하지 않습니다. 모든 투자의 책임은 개인에게 있으며, 이로 발생된 결과에 대해 어떤 부분에서도 Boom Labs는 책임을 지지 않습니다. 본문이 포괄하는 내용들은 특정 자산에 대한 투자를 추천하는 것이 아니며, 언제나 본문의 내용만을 통한 의사결정은 지양하시길 바랍니다.

초보자를 위한 Hardhat 튜토리얼

이 튜토리얼은 여러분들이 빠르게 무언가를 구현할 수 있도록 도와주는 것을 목표로 합니다.

이를 위해 이더리움 기반의 스마트 컨트랙트 개발을 도와주는 개발 환경 Hardhat을 사용할 것입니다. 개발자가 스마트 컨트랙트 및 dApp 구축 과정 중 반복 작업을 관리하고 자동화하는데 도움을 줍니다.

Hardhat은 개발시 도움을 주는 로컬 Ethereum 네트워크인 Hardhat Network도 포함하고 있습니다. 이를 통해 스마트 컨트랙트를 배포하고 테스트하고 코드를 디버깅 할 수 있습니다.

여기서는 아래와 같은 것들을 다룹니다.

  • 이더리움 개발을 위한 Node.js 환경 설정
  • Hardhat 프로젝트 생성 및 구성
  • 토큰을 구현하는 Solidity 스마트 컨트랙트의 기본 사항
  • Hardhat을 사용하여 스마트 컨트랙트에 대한 자동화된 테스트 작성
  • console.log() Hardhat Network를 사용하여 Solidity 디버깅
  • Hardhat Network 및 Ethereum 테스트넷에 스마트 컨트랙트 배포

이를 위해 아래와 같은 것들이 필요합니다!

  • Javascript로 코드를 작성하는 법 숙지
  • 터미널을 사용하는 법 숙지
  • git을 사용하는 법 숙지
  • 기본적으로 스마트 컨트랙트가 동작하는 원리 숙지
  • Metamask 지갑 셋팅

환경 설정하기

대부분의 이더리움 라이브러리와 도구는 Javascript로 작성되었으며 Hardhat도 마찬가지입니다. Node.js는 Chrome의 V8 Javascript 엔진을 기반으로 하는 Javascript 런타임입니다. 웹 브라우저 외부에서 Javascript를 실행하는 가장 인기 있는 솔루션이며 Hardhat이 그 위에 구축되어 있습니다.

Node.js 설치

Node.js가 설치되어 있는 경우 해당 과정은 생략 가능 합니다.

Mac OS

터미널에 다음 명령어를 복사하여 붙여넣습니다.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
nvm install 18
nvm use 18
nvm alias default 18
npm install npm --global # Upgrade npm to the latest version

Window

윈도우의 경우 해당 가이드를 참고하시기 바랍니다.

새로운 Hardhat 프로젝트 생성

npm은 JavaScrip 코드의 패키지 관리자이며 이를 이용하여 Hardhat을 설치합니다.

새 터미널을 열고 다음 명령을 실행하여 새 폴더를 만듭니다.

mkdir hardhat-tutorial
cd hardhat-tutorial

그런 다음 아래와 같이 npm 프로젝트를 초기화합니다.

npm init

이제 Hardhat을 설치할 수 있습니다.

npm install --save-dev hardhat

Hardhat을 설치한 동일한 디렉토리에서 다음을 실행하세요.

npx hardhat

“Create an empty hardhat.config.js”를 선택하고 Enter키를 누릅니다.

Hardhat이 실행되면 가장 가까운 hardhat.config.js 파일을 검색합니다.

Hardhat 아키텍쳐

Hardhat은 작업 및 플러그인 개념 중심으로 설계되어 있습니다. 대부분의 Hardhat의 기능들은 플러그인을 통해 제공됩니다.

작업(Tasks)

명령줄에서 Hardhat을 실행할 때 마다 작업을 실행하고 있는 것입니다. 예를 들어, npx hardhat compile

은 작업을 실행하고 컴파일하게 됩니다. 프로젝트에서 사용 가능한 작업 내역을 보기 위해서는 npx hardhat help [task] 를 실행하여 자세한 내용을 참고할 수 있습니다.

플러그인

Hardhat에는 어떤 도구를 사용하라는 가이드는 없지만 기본으로 몇가지 도구가 제공됩니다. 그리고 여러분은 이런 도구들을 재정의해서 사용 가능합니다. 여기서는 @nomicfoundation/hardhat-toolbox라는 권장 플러그인을 사용할 예정입니다. 여기에는 스마트 컨트랙트를 개발하는 데 필요한 모든 것이 포함되어 있습니다.

설치하기 위해 프로젝트 디렉토리에서 다음을 실행하면 됩니다.

npm install --save-dev @nomicfoundation/hardhat-toolbox

그리고 아까 생성된 hardhat.config.js파일로 들어가 아래 강조된 줄을 추가해야합니다.

require("@nomicfoundation/hardhat-toolbox");  ** @type import('hardhat/config').HardhatUserConfig */ module.exports = {   
solidity: "0.8.9",
};

스마트 컨트랙트 작성 및 컴파일

전송할 수 있는 토큰을 간단한 스마트 컨트랙트로 만들 것입니다. 토큰 컨트랙트는 가치를 교환하거나 저장하는 데 가장 자주 사용됩니다. 이 튜토리얼에서 구현할 몇 가지 로직은 다음과 같습니다.

  • 변경할 수 없는 고정된 총 토큰 공급량이 있습니다.
  • 전체 공급은 계약을 배포하는 주소에 할당됩니다.
  • 누구나 토큰을 받을 수 있습니다.
  • 토큰이 하나 이상 있는 사람은 누구나 토큰을 전송할 수 있습니다.
  • 토큰은 나눌 수 없습니다. 1, 2, 3 또는 37개의 토큰을 전송할 수 있지만 2.5는 전송할 수 없습니다.

스마트 컨트랙트 작성

contracts라는 폴더를 새로 생성하고 Token.sol이라는 파일을 생성합니다.

아래 코드를 붙여넣고 잠시 시간을 내어 코드를 읽어보세요.

//SPDX-License-Identifier: UNLICENSED// 솔리디티 파일은 pragma로 시작됩니다.
// 컴파일러는 이 부분을 보고 버전을 검증하게 됩니다.
pragma solidity ^0.8.9;
// 여기서부터 스마트 컨트랙트를 위한 주된 부분입니다.
contract Token {
// Some string type variables to identify the token.
string public name = "My Hardhat Token";
string public symbol = "MHT";
// 고정된 양의 토큰 수
uint256 public totalSupply = 1000000;
// 이더리움 계정을 저장하기 위해 address 타입의 변수를 선언합니다.
// mapping은 key/value 맵 형태의 자료구조 이며 여기에 각 계정의 잔고를 기록합니다.
mapping(address => uint256) balances;
// Transfer 이벤트는 오프체인 애플리케이션에서 스마트 컨트랙트에서 어떤 일이 일어났는지 이해하는 것을 도와줍니다.
event Transfer(address indexed _from, address indexed _to, uint256 _value);
/**
* 컨트랙트 초기화
*/
constructor() {
// totalSupply는 트랜잭션을 만든 사람의 계정에 주어집니다.
balances[msg.sender] = totalSupply;
address owner = msg.sender;
}
/**
* 토큰을 전송하기 위한 함수
*
* `external` modifier는 해당 함수가 외부에서만 호출 가능하도록 접근 제어 해줍니다.
*/
function transfer(address to, uint256 amount) external {
// 트랜잭션을 보내는 사람이 적당한 토큰을 가지고 있는지 확인합니다.
// 만약 require에서 첫 번째 인자가 "false"일 경우에는 해당 동작이 실패하고
// 트랜잭션은 초기화됩니다.
require(balances[msg.sender] >= amount, "Not enough tokens");
balances[msg.sender] -= amount;
balances[to] += amount;
// 오프체인 애플리케이션에 해당 내용을 전달해줍니다.
emit Transfer(msg.sender, to, amount);
}
/**
* 주어진 계정의 토큰 잔고를 읽기 위한 읽기 전용 함수
* view는 해당 함수의 상태를 변경시키지 않고 단순히 읽기만 하겠다는 의미이며
* 그렇기 때문에 가스비가 발생하지 않는다.
*/
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}

컨트랙트 컴파일

컨트랙트를 컴파일 하기 위해서 npx hardhat compile을 터미널에서 실행하세요. compile 작업은 기본적으로 제공해주는 작업 중 하나입니다.

$ npx hardhat compile
Compiling 1 file with 0.8.9
Compilation finished successfully

컨트랙트가 성공적으로 컴파일 되었으며 이제 이를 사용할 준비가 되었습니다.

컨트랙트 테스트하기

스마트 컨트랙트를 만들 때 자동화된 테스트를 하는 것은 사용자의 돈이 걸려 있기 때문에 매우 중요합니다.

스마트 컨트랙트를 테스트하기 위해 개발용으로 설계된 로컬 이더리움 네트워크인 Hardhat 네트워크를 사용할 것입니다. 이를 사용하기 위해 아무것도 설정할 필요가 없습니다.

쓰기 테스트

프로젝트 루트 디렉토리 내부에 test라는 새 폴더를 만들고 Token.js라는 새 파일을 만듭니다.

해당 파일에 아래 코드를 복사 붙여넣기 합니다.

const { expect } = require("chai");describe("Token contract", function () {
it("Deployment should assign the total supply of tokens to the owner", async function () {
const [owner] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");const hardhatToken = await Token.deploy();const ownerBalance = await hardhatToken.balanceOf(owner.address);
expect(await hardhatToken.totalSupply()).to.equal(ownerBalance);
});
});

터미널에서 npx hardhat test를 입력하면 아래와 같은 출력이 표시됩니다.

$ npx hardhat testToken contract
✓ Deployment should assign the total supply of tokens to the owner (654ms)
1 passing (663ms)

이것은 테스트가 통과되었음을 알려줍니다. 이제 각 줄을 설명해드리겠습니다.

const [owner] = await ethers.getSigners();

etheres.js의 Signer는 이더리움 계정을 나타내는 객체입니다. 컨트랙트 및 기타 계정으로 거래를 보내는 데 사용됩니다. 여기서 우리는 연결된 노드의 계정 목록을 얻습니다. 여기서는 Hardhat Network가 될 것이고 첫 번째 계정만 유지합니다.

etheres 변수는 전역 범위에서 사용할 수 있습니다. 코드가 항상 명시적이면 상단에 다음 줄을 추가할 수 있습니다.

const { ethers } = require("hardhat");const Token = await ethers.getContractFactory("Token");

ethere.js의 ContractFactory는 새로운 스마트 컨트랙트를 배포할 때 사용하는 추상화라고 볼 수 있으며 아까 만들었던 Token을 사용하게 됩니다.

const hardhatToken = await Token.deploy();

여기서 deploy()를 하면 ContractFactory 배포가 시작되고 Contract를 가진 Promise가 반환됩니다. 이 객체는 스마트 컨트랙트 함수 각각을 가지고 있게 됩니다.

const ownerBalance = await hardhatToken.balanceOf(owner.address);

다른 계정 사용하기

기본 계정이 아닌 다른 계정에서 트랜잭션을 전송하여 코드를 테스트해야 하는 경우 다음 conntect()와 같이 ether.js 객체의 메서드를 사용하여 다른 계정에 연결할 수 있습니다.

const { expect } = require("chai");describe("Token contract", function () {
// ...이전 테스트...
it("Should transfer tokens between accounts", async function() {
const [owner, addr1, addr2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");const hardhatToken = await Token.deploy();// 소유자로부터 addr1으로 토큰 50개 전송하기
await hardhatToken.transfer(addr1.address, 50);
expect(await hardhatToken.balanceOf(addr1.address)).to.equal(50);
// add1에서 add2로 토큰 50개 전송하기
await hardhatToken.connect(addr1).transfer(addr2.address, 50);
expect(await hardhatToken.balanceOf(addr2.address)).to.equal(50);
});
});

Hardhat 네트워크를 사용한 디버깅

Hardhat에는 개발용으로 설계된 로컬 Ethereum 네트워크인 Hardhat Network가 내장되어 있습니다. 이를 통해 로컬 시스템의 범위 내에서 컨트랙트를 배포하고 테스트를 실행하고 코드를 디버그할 수 있습니다. 연결되는 기본 네트워크 Hardhat이므로 작동하기 위해 아무 것도 설정할 필요가 없습니다. 테스트를 실행하면 됩니다.

console.log

Hardhat Network에서 컨트랙트 및 테스틀 실행할 때 console.log()를 통해 Solidity 코드에서 호출하는 로깅 메시지를 확인할 수 있습니다. 이를 사용하기 위해 hardhat/console.sol 컨트랙트 코드에서 가져와야합니다.

pragma solidity ^0.8.9;import "hardhat/console.sol";contract Token {
//...
}

그런 다음 Javascript에서 사용하는 것처럼 console.log 호출을 할 수 있습니다.

function transfer(address to, uint256 amount) external {
require(balances[msg.sender] >= amount, "Not enough tokens");
console.log(
"Transferring from %s to %s %s tokens",
msg.sender,
to,
amount
);
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transfer(msg.sender, to, amount);
}

그러면 테스트를 실행할 때 로그가 같이 표시됩니다.

$ npx hardhat testToken contract
Deployment
✓ Should set the right owner
✓ Should assign the total supply of tokens to the owner
Transactions
Transferring from 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 to 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 50 tokens
Transferring from 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 to 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc 50 tokens
✓ Should transfer tokens between accounts (373ms)
✓ Should fail if sender doesn’t have enough tokens
Transferring from 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 to 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 50 tokens
Transferring from 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 to 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc 50 tokens
✓ Should update balances after transfers (187ms)
5 passing (2s)

라이브 네트워크에 배포

다른 사람들에게 dApp을 공유할 준비가 되면 라이브 네트워크에 배포할 수 있습니다.

“메인넷” 이더리움 네트워크는 실제 돈을 다루지만, 그렇지 않은 별도의 “테스트넷” 네트워크가 있습니다. 이 테스트넷은 실제 돈을 걸지 않고 실제 상황과 유사한 공유 스테이징 환경을 제공합니다. 이번에는 ethere.js를 사용하여 스마트 컨트랙트를 배포하는 코드가 어떻게 생겼는지 살펴보겠습니다.

scripts라는 새 폴더를 만들고 deploy.js라는 파일을 생성합니다.

그리고 아래 코드를 붙여넣어 주세요.

async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);console.log("Account balance:", (await deployer.getBalance()).toString());const Token = await ethers.getContractFactory("Token");
const token = await Token.deploy();
console.log("Token address:", token.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

Hardhat이 특정 Ethereum 네트워크에 연결하도록 설정하기 위해 — network와 같이 작업을 실행할 때 매개변수를 사용합니다.

npx hardhat run scripts/deploy.js --network <network-name>

매개변수 없이 실행하면 Hadhat Network의 임베디드 인스턴스에 대해 실행됩니다.

$ npx hardhat run scripts/deploy.js
Deploying contracts with the account: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Account balance: 10000000000000000000000
Token address: 0x5FbDB2315678afecb367f032d93F642f64180aa3

원격 네트워크에 배포

메인넷이나 테스트넷과 같은 원격 네트워크에 배포하려면 hardhat.config.js 파일에 network 항목을 추가해야 합니다.

require("@nomicfoundation/hardhat-toolbox");// https://www.allthat로 가서 회원가입을 하고 새로운 앱을 생성합니다
// 그리고 "KEY" 부분에 API KEY를 넣어줍니다.
const ALCHEMY_API_KEY = "KEY";
// Georil 테스트 네트워크로 가서 메타마스크의 프라이빗 키를 추출합니다.
// 계정 세부 정보로 들어가 비공개 키 내보내기를 클릭하고 키를 추출합니다.
// 주의: 프라이빗 키를 공개된 장소에 업로드하지 마세요.
const GOERLI_PRIVATE_KEY = "YOUR GOERLI PRIVATE KEY";
module.exports = {
solidity: "0.8.9",
networks: {
goerli: {
url: `https://eth-goerli.alchemyapi.io/v2/${ALCHEMY_API_KEY}`,
accounts: [GOERLI_PRIVATE_KEY]
}
}
};

Georil에 배포하기 위해서는 배포할 주소로 Gorli ether를 보내야 합니다. test-ETH를 무료로 배포하는 서비스인 faucet에서 testnet ether를 얻을 수 있습니다.

마지막으로 다음을 실행합니다.

npx hardhat run scripts/deploy.js --network goerli

모든 것이 잘 수행되었다면 배포된 스마트 컨트랙트 주소가 표시됩니다.

(필수 X) 혹시 ethersan에서의 Contract Verify까지 원하신다면?!

아래 링크를 참고해주세요!

감사합니다!

--

--