Writing an ERC20 Token Contract#
이 튜토리얼에서는 첫 번째 스마트 컨트랙트를 작성합니다! OpenZeppelin을 사용하여 ERC-20 토큰 컨트랙트를 만들고, 특정 주소만 토큰을 발행할 수 있도록 권한을 추가하는 방법을 배웁니다.
ERC-20이란?#
ERC-20은 이더리움 블록체인에서 **대체 가능한 토큰(Fungible Token)**을 위한 기술 표준입니다.
- ERC: "Ethereum Request for Comment"의 약자로, 이더리움 개선 제안의 명명 규칙
- 20: 이 표준을 만든 제안 번호
ERC-20 토큰의 특징#
ERC-20 토큰은 이 표준에 정의된 특정 규칙을 따르는 암호화폐입니다:
- 토큰 전송 함수
- 잔액 확인 함수
- 제3자의 지출 승인 함수
이러한 표준화된 함수들은 ERC-20 토큰이 이더리움 생태계 전반에서 일관되게 작동하도록 보장하여, 쉽게 교환하고, 지갑에 저장하며, 탈중앙화 거래소에서 거래할 수 있게 합니다.
대표적인 예시: USDT (Tether), USDC, DAI 등
간단한 ERC20 토큰 작성하기#
Remix를 사용하여 ERC-20 토큰 컨트랙트를 작성합니다.
1단계: Remix에서 새 Workspace 생성#
- File Explorer 패널 상단에서 Workspaces 액션 버튼(햄버거 아이콘) 클릭
- Create blank 선택하여 새 워크스페이스 생성
- 워크스페이스 이름 지정 (예: "CLF")
- Ok 클릭

2단계: 새 폴더 생성#
- 파일 탐색기 사이드바에서 우클릭 → New Folder 클릭
- 폴더 이름:
contracts
3단계: 새 파일 생성#
contracts폴더에서 우클릭 → New file 클릭- 파일 이름:
MyERC20.sol

4단계: 코드 작성#
다음 토큰 컨트랙트 코드를 복사하여 붙여넣습니다:
💡 OpenZeppelin Wizard를 사용하여 토큰을 생성할 수도 있습니다. 또한 course code repo에서 코드를 가져올 수 있습니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import { ERC20 } from "@openzeppelin/contracts@5.2.0/token/ERC20/ERC20.sol";
contract MyERC20 is ERC20 {
constructor() ERC20("My Cyfrin CLF Token", "CLF") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
코드 설명#
-
OpenZeppelin ERC20 상속:
import로 가져오고is키워드로 상속 -
생성자에서 부모 ERC20 생성자 호출:
constructor() ERC20("My Cyfrin CLF Token", "CLF") {}
- 토큰 공급을 위한 public
mint함수 추가:
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
Access Control 추가하기#
현재 MyERC20 컨트랙트의 mint 함수는 누구나 호출할 수 있어서 무료로 토큰을 발행할 수 있습니다. 이를 변경하여 특정 주소에만 토큰 발행 권한을 부여하겠습니다.
OpenZeppelin AccessControl 사용하기#
OpenZeppelin의 AccessControl 스마트 컨트랙트를 사용하여 접근 제어를 추가합니다.
Roles 작동 방식#
- 각 역할(Role)에는 **역할 식별자(role identifier)**가 필요
- 역할 식별자는
bytes32constant변수 - 관례적으로 역할 이름의 해시된 문자열 사용 (상수이므로 대문자)
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
AccessControl 주요 함수#
| 함수 | 설명 |
|---|---|
grantRole | 역할 부여 |
hasRole | 주소가 역할을 가지고 있는지 확인 |
revokeRole | 역할 취소 |
Token Contract에 Minter Role 추가하기#
Step 1: AccessControl 컨트랙트 상속#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { ERC20 } from "@openzeppelin/contracts@4.6.0/token/ERC20/ERC20.sol";
import { AccessControl } from "@openzeppelin/contracts@4.6.0/access/AccessControl.sol";
contract MyERC20 is ERC20, AccessControl {
constructor() ERC20("My Cyfrin CLF Token", "CLF") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
Step 2: 역할 식별자 생성#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { ERC20 } from "@openzeppelin/contracts@4.6.0/token/ERC20/ERC20.sol";
import { AccessControl } from "@openzeppelin/contracts@4.6.0/access/AccessControl.sol";
contract MyERC20 is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() ERC20("My Cyfrin CLF Token", "CLF") {}
function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
Step 3: onlyRole modifier 추가#
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { ERC20 } from "@openzeppelin/contracts@4.6.0/token/ERC20/ERC20.sol";
import { AccessControl } from "@openzeppelin/contracts@4.6.0/access/AccessControl.sol";
contract MyERC20 is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() ERC20("My Cyfrin CLF Token", "CLF") {}
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}
이제 MINTER_ROLE을 가진 주소만 mint 함수를 호출할 수 있습니다!
DEFAULT_ADMIN 역할 부여하기#
현재 MINTER_ROLE을 부여할 방법이 없습니다. OpenZeppelin의 AccessControl에는 다른 주소에 역할을 부여할 수 있는 DEFAULT_ADMIN 역할이 있습니다.
최종 코드#
배포자(deployer)에게 생성자에서 DEFAULT_ADMIN_ROLE과 MINTER_ROLE을 모두 부여합니다:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import { ERC20 } from "@openzeppelin/contracts@4.6.0/token/ERC20/ERC20.sol";
import { AccessControl } from "@openzeppelin/contracts@4.6.0/access/AccessControl.sol";
contract MyERC20 is ERC20, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
constructor() ERC20("My Cyfrin CLF Token", "CLF") {
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}
function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) {
_mint(to, amount);
}
}
설명#
msg.sender: 생성자 컨텍스트에서 컨트랙트 배포자를 의미_grantRole: 역할과 역할을 부여할 주소를 인자로 받음DEFAULT_ADMIN_ROLE: 다른 주소에 역할을 부여할 수 있는 관리자 역할MINTER_ROLE: 토큰을 발행할 수 있는 역할