hardhat[오픈제플린 업그레이드]
hardhat과 오픈제플린 몇몇의 라이브러리를 가지고 업그레이드블한 컨트랙트를 배포해보자.
💡 hardhat은 무엇일까?
트러플과 유사한 이더리움 Dapp 개발도구
스마트 컨트랙트 작성, 컴파일, 테스트, 배포 도구
기존에 "Buidler"라는 이름으로 사용되었으나 "Hardhat"으로 변경
즉시 실행되는 내부(in-process) 가상 이더리움 네트워크 제공
web3.js 대신에 ethers.js를 사용합니다.
💡 hardhat test 폴더 생성 및 명령어 정리
1. 먼저 폴더를 만들 수 있도록합니다. mkidr {폴더명} 이렇게 만들든 그냥 GUI를 이용해서 만들든 상관은 없습니다.
2. 다음 cd 폴더명 / 또는 터미널의 위치를 생성한 폴더로 맞춰줄 수 있도록 합니다.(나의 폴더명 foolde)
3. yarn init -y || npm init -y 둘중 하나만 해서 package.json을 생성합니다.
4. yarn add hardhat || npm install hardhat
5. npx hardhat
* 설치를 완료하고나면 Create a basic sample project에 커서를 두고 enter로 처리해주면
artifacts -- 컴파일 결과물
contracts -- 컨트랙트 작성
scripts -- 배포 등의 실행 스크립트
test -- 단위 테스트
hardhat-config.js -- 개발환경설정(네트워크(rinkeby,ropsten)등등)
npx hardhat accounts 명령어 실행시 다음과 같은 내부 계정목록이 보이게 된다면 정상적으로 설치가 된것이다.
💡 Contract 작성
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract SimpleStorageUpgrade {
uint storedData;
event Change(string message, uint newval);
function set(uint x) public {
console.log("The value iss %d",x);
require(x < 5000, "Should be less than 5000");
storedData = x;
emit Change("set",x);
}
function get() public view returns (uint) {
return storedData;
}
}
💡 scripts 작성
여기서 중요하게 봐야하는 부분이 있다. 원래 배포를 하게 되면
const Greeter = await hre.ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello, Hardhat!");
이런식으로 하는데 우리는 proxy배포를 해서 지속적으로 업그레이드 가능하게 만들기 위한것이다. 업그레이드 컨트랙트를 작성하게되면 기존에 가지고있는 데이터를 버리지 않고 함수를 업데이트 할 수 있기 때문이다.
const SimpleStorageUpgrade = await hre.ethers.getContractFactory("SimpleStorageUpgrade")
const ssu = await upgrades.deployProxy(SimpleStorageUpgrade, [500], { initializer: 'set' });
안에들어가는 파라미터 부분은 컨트랙트를 받아온것과 초기 함수를 셋티하는 값을 넣어준 것이다.
다음 npx hardhat compile 명령어를 통해서 컨트랙트를 컴파일 해줄 수 있도록 하자.
const { upgrades } = require("hardhat");
const hre = require("hardhat");
async function main() {
//TODO
const SimpleStorageUpgrade = await hre.ethers.getContractFactory("SimpleStorageUpgrade")
const ssu = await upgrades.deployProxy(SimpleStorageUpgrade, [500], { initializer: 'set' });
console.log("SimpleStorageUpgrade deployed to:", ssu.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
💡 단위테스트
const hre = require('hardhat');
const { expect } = require("chai");
const { waffle } = require('hardhat');
describe("SimpleStorageUpgrade", function () {
const wallets = waffle.provider.getWallets();
before(async () => {
const signer = waffle.provider.getSigner(2);
const SimpleStorageUpgrade = await hre.artifacts.readArtifact("SimpleStorageUpgrade");
this.instance = await waffle.deployContract(signer, SimpleStorageUpgrade);
})
it("should change the value", async () => {
const tx = await this.instance.connect(wallets[1]).set(500);
const v = await this.instance.get();
expect(v).to.be.equal(500);
})
//revert reason
it("should revert", async () => {
await expect(this.instance.set(6000))
.to.be.revertedWith('Should be less than 5000')
})
})
npx hardhat test ./test/SimpleStorageUpgrade.js 명령어를 사용해 보면 아래와 같은 결과가 나와주면 컨트랙트도 테스트코드도 정상적으로 실행되고 있다는 것을 확인 할 수가 있음.
** 그전에 npm install chai를 해줘야 정상적으로 실행이 될것이다.
waffle.provider.getSigner(2); ⇒ 내부 네트워크의 계정을 가져오는 것
await hre.artifacts.readArtifact("SimpleStorageUpgrade"); ⇒ 조금전에 컴파일 했던 것을 가져오는
this.instance = await waffle.deployContract(signer, SimpleStorageUpgrade); ⇒ 단위테스트 하는 과정에서 컨트랙트를 배포하고 나서 단위테스트를 실제로 실행하는 것!
hardhat특징중 하나는 console.log를 사용할 수 있다는 장점이 있습니다.
import "hardhat/console.sol"; 를 해주어야 하구요 test용이기 때문에 테스트가 끝나고나면 log부분을 지워주어야 합니다.
즉 다시말해서 단위테스트에서 console.log()를 이용해서 어떤 값이 들어왔는지 확인해 볼 수 있습니다.
💡 로컬 네트워크에 배포 및 console에서 확인
hardhat-config.js 파일에 require('@openzeppelin/hardhat-upgrades'); 추가해줄 수 있도록 하자.
터미널 창을 2개를 키고 한곳에서 npx hardhat node를 실행.
배포된 컨트랙트를 유지하기 위해서는 npx hardhat node 명령을 실행시켜주어서 가나슈처럼 hardhat에 내재되어 있는 네트워크를 실행시킴으로써 실제 외부에서 접속가능한 가상 이더리움 네트워크가 실행이 됩니다.
npx hardhat run --network localhost ./scripts/SimpleStorageUpgrade.deploy.js 명령어 실행
localhost 네트워크에 배포가 잘 된것을 확인할 수가 있다.
이제 빠르게 콘솔을 열어서 제대로 값이 들어오는지 확인해 볼 수 있도록 합시다.
npx hardhat console --network localhost
const f = await ethers.getContractFactory("SimpleStorageUpgrade")
const ssu = f.attach("0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0") ==> 여기서 주소는 우리가 배포한 주소
주의할점은 proxy 주소라는 점을 생각하자.
(await ssu.get()).toString(). ==> 500이 나오면 정상적으로 된것이다.
이제 업그레이드 컨트랙트가 제대로 작동하는지 확인하기 위해서 값을 1000으로 변경시켜보자.
let tx = ssu.set("1000")
(await ssu.get()).toString() ==> 1000이 나와야합니다.
💡 컨트랙트 생성,배포 및 업그레이더블 확인
위에까지 따라왔으면 지금 우리는 proxy 컨트랙트를 배포한 상태가 되있을 것이다. 그리고 우리가 작성한 컨트랙트를 임플리먼트 컨트랙트라고 부를것이다. 그렇다면 우리가 작성한 임플리먼트 컨트랙트에 문제가 생겨서 변경하고 싶다면 우리는 어떻게 해야할 것인가?
먼저 SimpleStorageUpgradeV2.deploy.js 변경할 컨트랙트를 만들자.
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract SimpleStorageUpgradeV2 {
uint storedData;
uint storedKey;
event Change(string message, uint newval);
function set(uint x) public {
console.log("The value iss %d",x);
require(x < 5000, "Should be less than 5000");
storedData = x;
emit Change("set",x);
}
function get() public view returns (uint) {
return storedData;
}
function setKey(uint key) public {
storedKey = key;
}
function getKey() public view returns(uint){
return storedKey;
}
}
다음 스크립트도 하나 만들어 주자.
const { upgrades } = require("hardhat");
const hre = require("hardhat");
async function main() {
//TODO
const proxyAddress = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0";
const SimpleStorageUpgradeV2 = await hre.ethers.getContractFactory("SimpleStorageUpgradeV2");
const ssu2 = await upgrades.upgradeProxy(proxyAddress, SimpleStorageUpgradeV2);
console.log("SimpleStoageUpgrade deployed to ",ssu2.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
여기서 중요한 점은 const proxyAddress 이 변수명에 처음 배포했을때 받았던 proxcontract주소를 넣어줘야합니다.
그리고 자세히 보면 upgradeProxy로 변경된 것을 확인할 수가있다. 일단 이부분이 완료가 되었다면 다음은 우리가 배웠던 명령어를 통해서 확인해 볼 수가 있다.
그리고 배포를 진행하도록 하자 .
npx hardhat run --network localhost ./scripts/SimpleStorageUpgradeV2.deploy.js
그러면 놀랍게도 우리는 배포된 주소가 처음과 동일하다는 것을 확인할 수가있다.
다음 npx hardhat console --network localhost 콘솔을 열어준다음 확인을 해보자.
놀랍게도 우리가 처음 배포해서 값을 1000으로 바꾼 것이 사라지지 않고 그대로 존재하는 것을 확인할 수 가 있다. 그리고 우리가 만든 새로운 함수인 getKey함수도 사용할 수 있으며 여기서 setKey함수를 이용해서 값을 변경해 보고 다시 값을 출력해 보도록 하겠다.
우린 이로써 업그레이드 가능한 컨트랙트를 작성해보고 배포해 볼수있었다.
작성한 부분이 매끄럽지않아서 보기에 불편할 수 있을것같다. 그래서 업로드된 제파일을 가져다가 천천히 블로그와 비교해 보면서 뜯어보는것을 추천합니다. 이렇게 업그레이드 가능한 컨트랙트가 어떻게 가능한지에 대해서 조금 찾아보니 delegatecall을 활용해서 작성된것을 확인해 볼 수 있었다. 하지만 정확한 것은 아니다. 이부분에 대해서 더 자세히 찾아보고 올리도록 하겠다.
참고로 local 이아닌 ropsten 이나 rinkeby 테스트넷을 이용해 보고싶다면 hardhat-config.js부분에
이런식으로 작성해주고 --network rinkeby로 하면 잘 된다. url은 achemyapi 가서 api키를 발급받아야하고
account는 배포할 계정의 프라이빗키를 넣어주면된다.