일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- Vue.js
- ethers typescript
- 스마트컨트렉트 예약어 함수이름 중복
- vue기초
- 스마트컨트렉트 함수이름 중복 호출
- git rebase
- 러스트 기초
- multicall
- nestjs 튜토리얼
- 러스트기초
- 프록시배포구조
- chainlink 설명
- 스마트컨트렉트테스트
- 스마트컨트렉트프록시
- ethers v6
- ethers
- 컨트렉트 배포 자동화
- 깃허브명령어
- ambiguous function description
- nest.js설명
- rust 기초
- 스마트 컨트렉트 함수이름 중복
- Vue
- 러스트 기초 학습
- 체인의정석
- SBT표준
- ethers websocket
- ethers type
- 머신러닝기초
- 컨트렉트 동일한 함수이름 호출
- Today
- Total
체인의정석
ERC20 만들고 test code까지 작성하기 본문
1. 사용하려는 버전에 맞게 오픈제플린의 npm 모듈 다운로드 하고 Solidity 코드 작성해주기
먼저, ERC20의 경우 기본 자료형인 name, symbol, decimal을 배포 시에 지정해 주어야 한다.
토큰 자체를 만드는 거라면 최신버전으로 만들면 되지만 요즘엔 ERC20 정도는 테스트 용으로 하나씩 만들어 주는 경우가 많기 때문에 상황에 맞는 버전을 선택하는 것이 중요하다.
Solidity의 버전에 따라서 다르지만 (예를 들어 0.5.0 버전에서는 ERC20Detailed)
잘 찾아서 가져오면 된다.
이때 burn과 mint의 경우는 상속받은 후 public 함수로 선언하여 internal로 정의된 함수를 가져와서 선언해 주어야 한다.
마찬가지로 extension에 있는걸 상속받아서 사용해도 되지만 나는 extension에 정의된 걸 참고해서 그냥 직접 선언해 주는것을 선호한다.
그 결과 코드는 아래와 같이 나온다.
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol';
import './ownership/Ownable.sol';
contract ExampleERC20 is ERC20Detailed, Ownable, ERC20Pausable {
constructor(
string memory _name,
string memory _symbol,
uint8 _decimals
)public ERC20Detailed(_name, _symbol, _decimals)
{ }
function mint(address account, uint256 amount) public onlyOwner returns (bool) {
_mint(account, amount);
return true;
}
/**
* @dev Destoys `amount` tokens from the caller.
*
* See `ERC20._burn`.
*/
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
/**
* @dev See `ERC20._burnFrom`.
*/
function burnFrom(address account, uint256 amount) public {
_burnFrom(account, amount);
}
}
2. 배포 후 기본 값들 체크하기
테스트 코드에서는 배포를 먼저 하고 필요한 기본적인 값을 지정해 준다.
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { expect } from 'chai';
import chai from 'chai';
import { solidity } from 'ethereum-waffle';
import { Contract } from 'ethers';
import { ethers } from 'hardhat';
chai.use(solidity);
const name = 'ExampleToken';
const symbol = 'ET';
const decimals = 18;
function changeToBigInt(amount: number) {
const answerBigint = ethers.utils.parseUnits(amount.toString(), decimals);
return answerBigint;
}
이런식으로 선언 한번 해주고, 값들을 넣어준 후에 테스트에 필요할거 같은 함수를 만들어 준다.
changeToBigInt의 경우 처음에 상당히 골치아픈 부분인데 숫자가 커지면 숫자가 아닌 문자형으로 바뀌는 현상이 일어나기 때문에 이를 막아주기 위해서 amount를 받아온 후 ethers의 parseUnits를 통해서 바꾸어 준다.
가끔가다 decimal이 18이 아닌 경우도 있기 때문에 그땐 숫자를 바꿔주면 된다. 공식문서에서 weiToEther도 결국 parseUnits를 내부적으로 호출한다고 전에 본거 같다. 아무튼 이거 말고도 처음에 정의하고 싶은 함수가 있으면 정의해 준다.
describe('Start Example ERC20 test', async () => {
// contracts
let exampleERC20: Contract;
//signers
let owner: SignerWithAddress;
let addr1: SignerWithAddress;
let addr2: SignerWithAddress;
let amount: number;
it('Set data for ExampleERC721 test', async () => {
amount = 100;
[owner, addr1, addr2] = await ethers.getSigners(); // get a test address
});
테스트 코드가 시작될 때는 상위 스코프에서 정의한 변수가 계속 유지되기 때문에 값을 변경하며 테스트가 필요한 변수들은 맨 위에서 타입과 함께 정의해 주는것이 좋다. SingerWithAddress 타입의 경우 나중에 .connect를 써서 사인의 주체를 바꾸는 테스트도 할 수 있기 때문에 테스트에 필요하다면 전부 지정해주는 것이 중요하다. Contract 타입의 경우도 초기에 모두 지정해 주어야 컨트렉트를 불러와서 여러 함수를 테스트 할 수 있기 때문에 중요하다.
무엇보다 테스트 코드는 Descibe와 it을 반복적으로 순서에 맞추어서 잘 사용하는게 중요하다. 나는 초기 설정값은 it에 넣어주었다.
await ethers.getSigners()의 경우 초기에 테스트를 위한 계정들을 만들어 주는 함수로서 왼쪽에 배열을 넣고 필요한 만큼 계정들을 지정해 주면된다. 복잡한 테스트는 계정이 더 필요할 것이다.
describe('Test Example ERC721 Metadata', () => {
it('Should get correct name, symbol, decimal for the Example ERC20 Contract', async () => {
const ExampleERC20Factory = await ethers.getContractFactory('ExampleERC20');
exampleERC20 = await ExampleERC20Factory.deploy(name, symbol, decimals);
await exampleERC20.deployed();
expect(await exampleERC20.name()).to.equal(name);
expect(await exampleERC20.symbol()).to.equal(symbol);
expect(await exampleERC20.decimals()).to.equal(decimals);
});
});
첫번째 테스트 코드는 배포 후 기본 값들을 확인하는 것이다. 만약 생성자에 mint가 있다면 여기서 검사를 해주어야한다.
3. Test Mint & Transfer 그리고 approve
mint와 transfer를 테스트 하려는 경우 다음과 같이 진행한다.
describe('Test Transfer exampleERC20', () => {
it('Should get correct MetaData for the Example ERC20 Contract', async () => {
await expect(exampleERC20.mint(addr1.address, changeToBigInt(amount)))
.to.emit(exampleERC20, 'Transfer')
.withArgs(ethers.constants.AddressZero, addr1.address, changeToBigInt(amount));
expect(await exampleERC20.totalSupply()).to.equal(changeToBigInt(amount));
expect(await exampleERC20.balanceOf(addr1.address)).to.equal(changeToBigInt(amount));
});
});
describe('Test Approval exampleERC20', () => {
it('should get approved for the Example ERC20 Contract', async () => {
await expect(exampleERC20.connect(addr1).approve(addr2.address, changeToBigInt(amount)))
.to.emit(exampleERC20, 'Approval')
.withArgs(addr1.address, addr2.address, changeToBigInt(amount));
expect(await exampleERC20.allowance(addr1.address, addr2.address)).to.equal(changeToBigInt(amount));
});
});
실행시 아래와 같은 결과가 나온다.
Test Transfer exampleERC20
✓ Should get correct MetaData for the Example ERC20 Contract
Test Approval exampleERC20
✓ should get approved for the Example ERC20 Contract
3. TransferFrom 테스트
tranfsferFrom을 테스트 하기 위해서는 트랜잭션을 보내는 계정을 지정해서 테스트해야 한다.
describe('Test TransferFrom ExampleERC20', async () => {
it('Example ERC20 Contract should have erc20 token after TransferFrom', async () => {
await expect(exampleERC20.connect(addr2).transferFrom(addr1.address, owner.address, changeToBigInt(amount)))
.to.emit(exampleERC20, 'Transfer')
.withArgs(addr1.address, owner.address, changeToBigInt(amount));
expect(await exampleERC20.balanceOf(owner.address)).to.equal(changeToBigInt(amount));
});
});
다음과 같이 .connect(addr2)를 하면 2번 주소를 지정해서 함수를 실행할 수 있다.
4. Burn의 테스트
describe('Test burn exampleERC20', async () => {
it('Example ERC20 Contract should burn NFT clearly', async () => {
await expect(exampleERC20.connect(owner).burn(changeToBigInt(amount)))
.to.emit(exampleERC20, 'Transfer')
.withArgs(owner.address, ethers.constants.AddressZero, changeToBigInt(amount));
expect(await exampleERC20.balanceOf(owner.address)).to.equal(0);
});
});
소각의 경우 0번 주소로 보내는 행위를 하므로 etheres.constants.AddressZero를 사용하여 0 번 주소로 보내는 이벤트 로그를 찍어준다.
5. 정리
전체 테스트 코드
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-misused-promises */
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers';
import { expect } from 'chai';
import chai from 'chai';
import { solidity } from 'ethereum-waffle';
import { Contract } from 'ethers';
import { ethers } from 'hardhat';
chai.use(solidity);
const name = 'ExampleToken';
const symbol = 'ET';
const decimals = 18;
function changeToBigInt(amount: number) {
const answerBigint = ethers.utils.parseUnits(amount.toString(), decimals);
return answerBigint;
}
describe('Start Example ERC20 test', async () => {
// contracts
let exampleERC20: Contract;
//signers
let owner: SignerWithAddress;
let addr1: SignerWithAddress;
let addr2: SignerWithAddress;
let amount: number;
it('Set data for exampleERC20 test', async () => {
amount = 100;
[owner, addr1, addr2] = await ethers.getSigners(); // get a test address
});
describe('Test Example ERC20 Metadata', () => {
it('Should get correct name, symbol, decimal for the Example ERC20 Contract', async () => {
const ExampleERC20Factory = await ethers.getContractFactory('ExampleERC20');
exampleERC20 = await ExampleERC20Factory.deploy(name, symbol, decimals);
await exampleERC20.deployed();
expect(await exampleERC20.name()).to.equal(name);
expect(await exampleERC20.symbol()).to.equal(symbol);
expect(await exampleERC20.decimals()).to.equal(decimals);
});
});
describe('Test Transfer exampleERC20', () => {
it('Should get correct MetaData for the Example ERC20 Contract', async () => {
await expect(exampleERC20.mint(addr1.address, changeToBigInt(amount)))
.to.emit(exampleERC20, 'Transfer')
.withArgs(ethers.constants.AddressZero, addr1.address, changeToBigInt(amount));
expect(await exampleERC20.totalSupply()).to.equal(changeToBigInt(amount));
expect(await exampleERC20.balanceOf(addr1.address)).to.equal(changeToBigInt(amount));
});
});
describe('Test Approval exampleERC20', () => {
it('should get approved for the Example ERC20 Contract', async () => {
await expect(exampleERC20.connect(addr1).approve(addr2.address, changeToBigInt(amount)))
.to.emit(exampleERC20, 'Approval')
.withArgs(addr1.address, addr2.address, changeToBigInt(amount));
expect(await exampleERC20.allowance(addr1.address, addr2.address)).to.equal(changeToBigInt(amount));
});
});
describe('Test TransferFrom ExampleERC20', () => {
it('Example ERC20 Contract should have erc20 token after TransferFrom', async () => {
await expect(exampleERC20.connect(addr2).transferFrom(addr1.address, owner.address, changeToBigInt(amount)))
.to.emit(exampleERC20, 'Transfer')
.withArgs(addr1.address, owner.address, changeToBigInt(amount));
expect(await exampleERC20.balanceOf(owner.address)).to.equal(changeToBigInt(amount));
});
});
describe('Test burn exampleERC20', () => {
it('Example ERC20 Contract should burn erc20 token clearly', async () => {
await expect(exampleERC20.connect(owner).burn(changeToBigInt(amount)))
.to.emit(exampleERC20, 'Transfer')
.withArgs(owner.address, ethers.constants.AddressZero, changeToBigInt(amount));
expect(await exampleERC20.balanceOf(owner.address)).to.equal(0);
});
});
});
전체 테스트 결과
Start Example ERC20 test
✓ Set data for exampleERC20 test
Test Example ERC20 Metadata
✓ Should get correct name, symbol, decimal for the Example ERC20 Contract
Test Transfer exampleERC20
✓ Should get correct MetaData for the Example ERC20 Contract
Test Approval exampleERC20
✓ should get approved for the Example ERC20 Contract
Test TransferFrom ExampleERC20
✓ Example ERC20 Contract should have erc20 token after TransferFrom
Test burn exampleERC20
✓ Example ERC20 Contract should burn erc20 token clearly
·---------------------------------|----------------------------|-------------|-----------------------------·
| Solc version: 0.5.5 · Optimizer enabled: false · Runs: 200 · Block limit: 30000000 gas │
··································|····························|·············|······························
| Methods │
·················|················|··············|·············|·············|···············|··············
| Contract · Method · Min · Max · Avg · # calls · usd (avg) │
·················|················|··············|·············|·············|···············|··············
| ExampleERC20 · approve · - · - · 46274 · 2 · - │
·················|················|··············|·············|·············|···············|··············
| ExampleERC20 · burn · - · - · 27171 · 2 · - │
·················|················|··············|·············|·············|···············|··············
| ExampleERC20 · mint · - · - · 70884 · 2 · - │
·················|················|··············|·············|·············|···············|··············
| ExampleERC20 · transferFrom · - · - · 49998 · 2 · - │
·················|················|··············|·············|·············|···············|··············
| Deployments · · % of limit · │
··································|··············|·············|·············|···············|··············
| ExampleERC20 · - · - · 2086327 · 7 % · - │
·---------------------------------|--------------|-------------|-------------|---------------|-------------·
6 passing (3s)
글쓴이 유튜브 채널 : https://www.youtube.com/channel/UCHsRy47P2KlE749oAAjb0Yg
'블록체인 > Ethers & web3' 카테고리의 다른 글
Typescript에서 hardhat 사용하기 - task 사용하는법, config파일 밖으로 꺼내서 코드 가독성 높이는 법 (0) | 2022.12.16 |
---|---|
ethers & hardhat에서 이벤트 로그 다루기 (검색부터 필터링과 decode) (1) | 2022.11.23 |
스마트컨트렉트 테스트 코드, hardhat & ethers & Typescript에서 상황별 Big number 다루기 (0) | 2022.07.26 |
hardhat 테스트 코드에서 beforeEach 사용하기 VS fixtures 사용하기 (0) | 2022.07.19 |
ethers에서 bigNumber를 number 형태로 고치기 + 테스트 값 비교 (0) | 2022.07.04 |