시작: 토큰을 만들고 배포하기

몇 주 전, 나는 ERC-20 토큰을 직접 만들어서 운영해보기로 했다. 교육 목적으로 시작한 프로젝트인데, 생각보다 배울 점이 많았다.

왜 했을까?

  • Ethereum과 스마트 컨트랙트의 실제 작동 원리 이해
  • DeFi 기초 개념 습득
  • 프로덕션 레벨의 보안 고려사항 체험

아키텍처: 멀티레이어 보안

처음부터 보안을 중심으로 설계했다.

┌─────────────────────────────────────────┐
│         Internet (HTTPS)                │
│     chloetoken.yeonghoon.kim            │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│    Caddy WAF (Coraza)                   │
│  ├─ IP 접근 제한                        │
│  ├─ SQL Injection 감지                  │
│  └─ XSS 방어                            │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│    Traefik (Reverse Proxy)              │
│  ├─ 라우팅                              │
│  └─ 로드 밸런싱                         │
└──────────────┬──────────────────────────┘
               │
        ┌──────┴──────┐
        │             │
┌───────▼──┐   ┌──────▼─────┐
│ Frontend  │   │  Backend   │
│ (Nginx)   │   │  (FastAPI) │
└───────┬──┘   └──────┬─────┘
        │             │
        │      ┌──────▼──────┐
        │      │ PostgreSQL  │
        │      │ (Tx History)│
        │      └─────────────┘
        │
        └─────▶ Ethereum RPC
               (Sepolia Testnet)

보안 레이어:

  1. WAF (Web Application Firewall)

    • Caddy + Coraza로 SQL Injection, XSS 탐지
    • 악성 봇 User-Agent 차단
  2. IP 접근 제한

    • 허용된 IP만 접근 가능
    • 관리 IP 4개만 등록
  3. 환경변수 관리

    • 개인키는 .env에 저장 (Git 제외)
    • 프론트엔드에서 입력 불가
    • 백엔드에서만 사용
  4. API 설계

    • 프론트엔드는 수신 주소/금액/메모만 전송
    • 백엔드에서 서명 및 전송

스마트 컨트랙트: 간단하고 안전하게

ERC-20 표준을 기반으로 하되, 필요한 기능만 구현했다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

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

contract ChloeToken is ERC20, Ownable {
    uint256 public constant INITIAL_SUPPLY = 1_000_000 * 10**18;

    constructor() ERC20("ChloeToken", "CHT") Ownable(msg.sender) {
        _mint(msg.sender, INITIAL_SUPPLY);
    }

    // 관리자 전용: 추가 발행
    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }

    // 관리자 전용: 소각
    function burn(uint256 amount) public onlyOwner {
        _burn(msg.sender, amount);
    }
}

특징:

  • OpenZeppelin 검증된 라이브러리 사용
  • Ownable 패턴으로 권한 관리
  • 초기 발행 1,000,000 CHT
  • mint/burn 기능으로 유연한 운영

배포: 아키텍처의 통합

1. 컨트랙트 배포

1
2
3
4
5
# Sepolia 테스트넷에 배포
# 가스비: ~0.05 ETH

컨트랙트 주소: 0x[배포된 주소]
배포자: 0x[배포자 지갑] (owner)

2. Docker 컨테이너화

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
services:
  postgres:
    image: postgres:15-alpine
    # 거래 내역 저장
    
  backend:
    build: ./backend
    environment:
      - SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/...
      - ADMIN_PRIVATE_KEY=${ADMIN_PRIVATE_KEY}
    
  frontend:
    build: ./frontend
    # Nginx에서 정적 파일 서빙

3. Traefik 라우팅

1
2
3
4
# docker-compose.yml에서
labels:
  - "traefik.http.routers.chloetoken.rule=Host(`chloetoken.yeonghoon.kim`)"
  - "traefik.http.routers.chloetoken.entrypoints=web"

4. Caddy WAF 설정

chloetoken.yeonghoon.kim {
  @allowed remote_ip [관리자 IP]/32 [회사 IP]/26 ...
  
  handle @allowed {
    # 허용된 IP만 접근
    reverse_proxy traefik:8080
  }
  
  handle {
    respond "Forbidden" 403
  }
}

핵심 기능: 거래와 메모

토큰 전송

프론트엔드 → 백엔드 요청:

1
2
3
4
5
{
  "to_address": "0x[수신자_주소]",
  "amount": "100.5",
  "memo": "Test Transfer #1"
}

백엔드 처리:

  1. to_address 유효성 검증
  2. 관리자 개인키로 거래 서명
  3. Ethereum RPC에 전송
  4. 거래 영수증 대기
  5. PostgreSQL에 거래 내역 저장

응답:

1
2
3
4
5
6
7
8
{
  "transactionHash": "0x...",
  "from": "0x...",
  "to": "0x...",
  "value": "100.5",
  "blockNumber": 6054789,
  "status": "success"
}

메모 기능

거래 추적 및 목적 명시를 위해 메모 필드를 추가했다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# models.py
class TransferRequest(BaseModel):
    to_address: str
    amount: float
    memo: Optional[str] = None

# main.py
transaction = db_models.Transaction(
    tx_hash=receipt['transactionHash'].hex(),
    from_address=settings.ADMIN_ADDRESS,
    to_address=request.to_address,
    amount=request.amount,
    memo=request.memo,  # ← 메모 저장
    status="confirmed"
)

배운 점: 블록체인 개발의 현실

1. 환경변수 관리의 중요성

처음엔 프론트엔드에서 개인키를 입력받으려 했다. 🙅

1
2
// ❌ 위험: 프론트엔드에서 개인키 입력
const privateKey = document.getElementById('privateKey').value;

결국 백엔드 환경변수로 변경했다:

1
2
# ✅ 안전: 백엔드에서만 로드
ADMIN_PRIVATE_KEY = os.getenv('ADMIN_PRIVATE_KEY')

배운 점: “클라이언트 사이드 서명은 이론은 좋지만, 실무에선 복잡한 보안 고려사항이 많다.”

2. RPC 호출의 성능

처음엔 프론트엔드에서 직접 RPC를 호출했다:

1
2
// ❌ 느림: 브라우저에서 RPC 직접 호출
const balance = await ethers.provider.getBalance(address);

나중에 백엔드 API로 변경:

1
2
// ✅ 빠름: 백엔드 API 사용
const response = await fetch('/api/v1/balance/' + address);

배운 점: “프론트엔드는 가능한 한 가볍게, 비즈니스 로직은 백엔드에서.”

3. Testnet의 중요성

Sepolia에서 충분히 테스트했기 때문에 프로덕션 배포가 수월했다.

  • 거래 시뮬레이션 가능
  • 가스비 부담 없음 (Faucet)
  • 블록체인 동작 이해

4. WAF가 생각보다 중요하다

IP 접근 제한만으로도 원치 않는 접근을 99% 차단할 수 있다.

Before: 스캔 봇들의 계속된 노이즈
After: 허용된 IP만 접근 → 조용함

남은 과제

현재 상태:

  • ✅ 컨트랙트 배포
  • ✅ 프로덕션 배포
  • ✅ IP 접근 제한
  • ✅ 거래 메모 기능
  • ⏳ 토큰 발행 (개인키 확인 대기)
  • ⏳ v2 컨트랙트 검토

다음 단계:

  1. 관리자 지갑에서 신규 지갑으로 owner 권한 이전 (transferOwnership)
  2. 신규 지갑에서 CHT 발행 (mint)
  3. 엔드-투-엔드 테스트
  4. Etherscan 검증

결론: 블록체인은 보안 먼저

이 프로젝트를 통해 배운 가장 중요한 교훈은:

“블록체인에서 돈을 다루는 순간, 보안은 선택이 아니라 필수다.”

작은 테스트넷이지만, 마치 메인넷처럼 다뤘다.

  • 개인키는 환경변수로
  • 프론트엔드는 서명 불가
  • 모든 접근은 IP로 제한
  • WAF로 공격 탐지

이런 습관이 프로덕션으로 나아갈 때 든든하다.

다음 글에서는 v2 컨트랙트와 스왑 기능을 소개할 예정이다. 😊


참고 자료: