ERC-20 토큰을 A부터 Z까지 만들기 (Hardhat + OpenZeppelin)

발행: (2025년 12월 18일 오전 10:45 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

Disclaimer
교육 목적만을 위한 것입니다. 금전적 가치가 없으며, 실제 브랜드나 스테이블코인과 연관되지 않습니다.

왜 이 글을 썼는가

많은 사람들이 암호화폐에 입문하면서 같은 질문을 합니다:

  • “왜 내 지갑에 무작위 토큰이 나타났나요?”
  • “잔액은 보이는데 왜 팔 수 없나요?”
  • “ERC‑20 토큰은 실제로 어떻게 생성되나요?”
  • “가짜 토큰인데 왜 진짜처럼 보이나요?”

이를 진정으로 이해하는 가장 쉬운 방법은 한 번이라도 직접 ERC‑20 토큰을 만들어 보는 것입니다. 직접 만들면 모든 것이 갑자기 이해됩니다: 잔액, 지갑, 승인, 가짜 토큰, 승인 과정 등.

우리가 만들게 될 것

우리는 다음과 같은 깔끔한 교육용 ERC‑20 토큰을 만들 것입니다:

  • OpenZeppelin ERC‑20 구현
  • 역할 기반 접근 제어
  • 일시 중지 가능한 전송
  • 토큰 발행
  • Hardhat을 이용한 배포
  • MetaMask에 토큰 추가
  • 지갑이 잔액을 감지하는 방식 이해

1. ERC‑20란 무엇인가 (쉬운 설명)

ERC‑20는 통화가 아닙니다. 최소한 ERC‑20 토큰은 다음과 같은 함수를 구현합니다:

function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function totalSupply() external view returns (uint256);
function name() external view returns (string);
function symbol() external view returns (string);
function decimals() external view returns (uint8);

Important
토큰이 가격과 실제 가치를 가지려면 시장과 유동성이 존재해야 합니다. 유동성이 없으면 → 가격이 없고 → 실제 가치도 없습니다.

2. 환경 설정

요구 사항

  • Node.js (LTS)
  • npm
  • MetaMask 지갑
  • Sepolia 테스트 ETH
  • RPC 제공자 (Alchemy, Infura 등)

프로젝트 생성

mkdir my-token
cd my-token
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox dotenv
npx hardhat

프롬프트가 표시되면 Create a JavaScript project를 선택하세요.

3. OpenZeppelin 설치

OpenZeppelin은 감사를 거친, 프로덕션 등급의 스마트 계약을 제공합니다.

npm install @openzeppelin/contracts

Source:

4. ERC‑20 토큰 계약 작성

contracts/MyToken.sol 파일을 생성합니다:

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

/*
    MyToken — an educational ERC-20 token.
    NOT money. NOT a stablecoin.
*/

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/Pausable.sol";

contract MyToken is ERC20, AccessControl, Pausable {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant PAUSER_ROLE = keccak256("PAUSER_ROLE");

    constructor(uint256 initialSupply)
        ERC20("My Educational Token", "MYT")
    {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        _grantRole(MINTER_ROLE, msg.sender);
        _grantRole(PAUSER_ROLE, msg.sender);

        _mint(msg.sender, initialSupply);
    }

    function mint(address to, uint256 amount)
        external
        onlyRole(MINTER_ROLE)
    {
        _mint(to, amount);
    }

    function pause() external onlyRole(PAUSER_ROLE) {
        _pause();
    }

    function unpause() external onlyRole(PAUSER_ROLE) {
        _unpause();
    }

    function _update(address from, address to, uint256 amount)
        internal
        override
        whenNotPaused
    {
        super._update(from, to, amount);
    }
}

핵심 개념

  • namesymbol은 단순히 문자열일 뿐입니다.
  • 계약은 잔액을 저장합니다.
  • 토큰은 발행(민트)된 이후에만 존재합니다.
  • 발행하지 않으면 잔액은 0이 됩니다.

5. Hardhat 구성

hardhat.config.js 파일을 만들거나(편집) 합니다:

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

module.exports = {
  solidity: "0.8.20",
  networks: {
    sepolia: {
      url: process.env.SEPOLIA_RPC,
      accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
    },
  },
};

6. 환경 변수

Create a .env file (never commit this file):

SEPOLIA_RPC=https://eth-sepolia.g.alchemy.com/v2/YOUR_API_KEY
PRIVATE_KEY=0xYOUR_PRIVATE_KEY

7. 배포 스크립트

​scripts/deploy.js​ 파일을 생성합니다:

const hre = require("hardhat");

async function main() {
  const [deployer] = await hre.ethers.getSigners();

  console.log("Deploying with address:", deployer.address);

  const initialSupply = hre.ethers.parseUnits("1000", 18);

  const MyToken = await hre.ethers.getContractFactory("MyToken");
  const token = await MyToken.deploy(initialSupply);

  await token.waitForDeployment();

  console.log("Token deployed at:", await token.getAddress());
}

main().catch(console.error);

8. 컴파일 및 배포

npx hardhat compile
npx hardhat run scripts/deploy.js --network sepolia

계약 주소를 받게 됩니다.

9. 토큰을 MetaMask에 추가하기

  1. MetaMask를 Sepolia 네트워크로 전환합니다.
  2. Assets → Import Token 으로 이동합니다.
  3. 계약 주소를 붙여넣습니다. MetaMask가 자동으로 심볼과 소수점을 채워줍니다.

잔액이 0으로 표시되면 다음을 확인하세요:

  • 올바른 네트워크인지
  • 토큰을 발행한 주소가 실제로 토큰을 받았는지
  • 소수점 설정
  • MetaMask를 새로 고침(캐시 삭제)

10. 토큰이 “갑자기” 나타날 수 있는 이유

지갑은 토큰을 저장하지 않습니다. 지갑은 단순히 다음을 호출합니다:

balanceOf(yourAddress)

따라서 누구든지 당신의 주소로 토큰을 허가나 요청 없이 발행할 수 있으며, 지갑은 여전히 그것을 표시합니다.

11. 이름, 심볼, 아이콘은 아무 의미가 없다

  • 토큰 이름에 대한 전역 레지스트리가 없습니다.
  • 온체인에서 상표 집행이 이루어지지 않습니다.
  • “공식 토큰” 플래그가 없습니다.

누구든지 이름, 심볼, 소수점 자리수, 로고를 마음대로 지정하여 계약을 배포할 수 있습니다. 유일하게 신뢰할 수 있는 토큰 식별자는 계약 주소입니다.

12. 핵심 보안 교훈

이 기본 원칙들을 이해하면 많은 사기를 쉽게 알 수 있습니다:

  • “Free tokens” → 의미 없는 잔액
  • 가짜 스테이블코인 → 멋진 이름을 가진 ERC‑20 계약일 뿐
  • 승인 함정 → 위험한 권한

지식이 두려움을 이깁니다.

결론

ERC‑20 토큰은 단순히 스마트 계약일 뿐입니다. 직접 토큰을 한 번 만들어 보면 잔액, 승인, 발행 및 이를 둘러싼 보안 함정을 이해하는 가장 빠른 방법입니다.

Back to Blog

관련 글

더 보기 »