[SCH] Smart Contract Hacking 2편 - Token

0xDave·2023년 3월 17일
0

Ethereum

목록 보기
94/112

첫 번째 과제



1. 컨트랙트 만들기


토큰 컨트랙트 만드는 건 Openzepplin wizard를 이용하면 쉽게 만들 수 있다. 토큰 이름과 심볼을 적어주고 입맛에 맞게 Mintable, Burnerble, Ownable에 체크하고 복붙해주면 끝.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract ExToken is ERC20, ERC20Burnable, Ownable {
    constructor() ERC20("ExToken", "EX") {}

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}

2. 테스트 코드 짜기


const { ethers } = require('hardhat');
const { expect } = require('chai');

describe('ERC20 Tokens Exercise 1', function () {
    
    let deployer, user1, user2, user3;

    // Constants 
    const DEPLOYER_MINT = ethers.utils.parseEther('100000');
    const USERS_MINT = ethers.utils.parseEther('5000');
    const FIRST_TRANSFER = ethers.utils.parseEther('100');
    const SECOND_TRANSFER = ethers.utils.parseEther('1000');

    before(async function () {
        /** Deployment and minting tests */
        
        [deployer, user1, user2, user3] = await ethers.getSigners();

        // TODO: Contract deployment
        
        // TODO: Minting
        
        // TODO: Check Minting

    });

    it('Transfer tests', async function () {
        /** Transfers Tests */

        // TODO: First transfer

        // TODO: Approval & Allowance test

        // TODO: Second transfer

        // TODO: Checking balances after transfer
        
    });
});

제공되는 기본 틀의 빈 공간을 채워보자.

최종 테스트 코드


const { ethers } = require("hardhat");
const { expect } = require("chai");

describe("ERC20 Tokens Exercise 1", function () {
  let deployer, user1, user2, user3;

  // Constants
  const DEPLOYER_MINT = ethers.utils.parseEther("100000");
  const USERS_MINT = ethers.utils.parseEther("5000");
  const FIRST_TRANSFER = ethers.utils.parseEther("100");
  const SECOND_TRANSFER = ethers.utils.parseEther("1000");

  before(async function () {
    /** Deployment and minting tests */

    [deployer, user1, user2, user3] = await ethers.getSigners();

    // TODO: Contract deployment
    const TokenFactory = await ethers.getContractFactory("ExToken", deployer);
    this.token = await TokenFactory.deploy();

    // TODO: Minting
    await this.token.mint(deployer.address, DEPLOYER_MINT);
    await this.token.mint(user1.address, USERS_MINT);
    await this.token.mint(user2.address, USERS_MINT);
    await this.token.mint(user3.address, USERS_MINT);
    // TODO: Check Minting
    expect(await this.token.balanceOf(deployer.address)).to.equal(
      DEPLOYER_MINT
    );
    expect(await this.token.balanceOf(user1.address)).to.equal(USERS_MINT);
    expect(await this.token.balanceOf(user2.address)).to.equal(USERS_MINT);
    expect(await this.token.balanceOf(user3.address)).to.equal(USERS_MINT);
  });

  it("Transfer tests", async function () {
    /** Transfers Tests */

    // TODO: First transfer
    await this.token.connect(user2).transfer(user3.address, FIRST_TRANSFER);

    // TODO: Approval & Allowance test
    await this.token.connect(user3).approve(user1.address, SECOND_TRANSFER);
    expect(await this.token.allowance(user3.address, user1.address)).to.equal(
      SECOND_TRANSFER
    );

    // TODO: Second transfer
    await this.token
      .connect(user1)
      .transferFrom(user3.address, user1.address, SECOND_TRANSFER);

    // TODO: Checking balances after transfer
    expect(await this.token.balanceOf(deployer.address)).to.equal(
      DEPLOYER_MINT
    );
    expect(await this.token.balanceOf(user1.address)).to.equal(
      USERS_MINT.add(SECOND_TRANSFER)
    );
    expect(await this.token.balanceOf(user2.address)).to.equal(
      USERS_MINT.sub(FIRST_TRANSFER)
    );
    expect(await this.token.balanceOf(user3.address)).to.equal(
      USERS_MINT.add(FIRST_TRANSFER).sub(SECOND_TRANSFER)
    );
  });
});

이제 npm run erc20-1으로 테스트 해주면 된다. Johnny가 테스트 script를 아래처럼 짜놔서 위 명령어로 테스트가 가능하다. package.json을 보면 요런식으로 미리 만들어놓은 것을 알 수 있다.

테스트 성공!


피드백


  1. 처음에 아예 컨트랙트를 컴파일해서 디플로이 먼저 한 다음에 테스트 코드를 돌리는 줄 알았다. 그런데 그냥 테스트 코드를 실행하면 알아서 컴파일 되고 테스트까지 진행되는 거였다. 테스트 코드 경험이 부족하다는 것을 다시 한 번 깨닫게 됐다..

  1. Johnny가 처음 제공한 틀에서 이더 갯수를 사용하기 쉽게 //Constants로 만들어놓았다. 이걸 지나치고 갯수를 바로 입력하려고 하니까 코드가 많이 지저분해졌었다. 다음에 테스트코드를 짤 때는 가이드 받은 것처럼 이더 갯수를 미리 const로 만들어놓자.
  // Constants
  const DEPLOYER_MINT = ethers.utils.parseEther("100000");
  const USERS_MINT = ethers.utils.parseEther("5000");
  const FIRST_TRANSFER = ethers.utils.parseEther("100");
  const SECOND_TRANSFER = ethers.utils.parseEther("1000");

  1. 무작정 앞에 const를 쓰려 했다. 정말 부끄럽지만 잘못된 코드를 다시 쓰지 않기 위해 기록해두려 한다. 예를 들어 아래처럼 단순히 토큰을 민팅하는 코드인데도 불구하고 따로 변수에 저장하려고 했다. 내가 쓰는 코드의 목적이 무엇인지 반드시 먼저 생각하고 코드로 옮기려고 하자.
const mint1 = await this.token.mint(user1.address, USERS_MINT);

  1. 계속 파라미터 관련 에러가 났었다. 나중에 알고보니 mint() 또는 balanceOf()에 파라미터 설정을 잘못했었다. address까지 입력해야 하는데 그냥 Singer를 입력했던 것. 더 혼란스러웠던 건 connect()에는 address 없이 Signer만 입력해야 한다.
await this.token.balanceOf(user1.address);

await this.token.connect(user2).transfer(user3.address, FIRST_TRANSFER);

혼란스러울 때는 공식문서를 찾아보는 것이 최고다. Openzeppelin의 IERC20을 보면 파라미터에 address를 넣으라고 명시되어 있다. 그러니 Signer를 넣는 것은 애초에 말이 안 되는 행동이었다.

다음은 ethers 공식문서에서 connect를 찾아봤다. 역시나 address가 아니라 Signer를 넣으라고 명시되어 있다. 항상 답은 공식문서에 있다는 것을 잊지 말자.


profile
Just BUIDL :)

0개의 댓글