체인의정석

ERC20 만들고 test code까지 작성하기 본문

블록체인/Ethers & web3

ERC20 만들고 test code까지 작성하기

체인의정석 2022. 8. 11. 14:30
728x90
반응형

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

 

체인의정석

약력 현) 블록체인 개발자 前 블록워터 테크놀로지, 스마트컨트렉트 개발자 前 위데이터랩(주) 기획,마케팅 팀장 , 블록체인팀 선임연구원 홍익대학교 경영학 전공, 컴공 부전공 서강대학교 정

www.youtube.com

 

728x90
반응형
Comments