# Faucet 만들어보기 1: 컨트랙트를 배포해보자
직접 ERC-20 토큰을 생성하고, 해당 토큰을 제공해주는 Faucet을 만들어봅니다.
해당 과정을 통해 기본적인 컨트랙트를 생성하고, 컨트랙트와 통신하는 방법을 이해합니다.
---
# 기본 설정
기본적으로 다음과 같은 프로그램이 필요합니다.
- [Rust](https://www.rust-lang.org/)
- [Foundry](https://book.getfoundry.sh/getting-started/installation)
- [Visual Studio Code](https://code.visualstudio.com/)
## 프로젝트 세팅하기
커맨드 라인을 열고, 원하는 폴더에서 `forge init` 명령어를 호출합니다.
```
forge init project_name
```
해당 디렉토리에 `project_name`이라는 폴더가 생성됩니다. 새로운 솔리디티 프로젝트입니다.
```
.
├── lib
├── script
├── src
└── test
```
디렉토리 구조는 다음과 같으며, 각자 다음과 같은 역할을 합니다.
- `lib`: 개발자가 추가한 솔리디티 라이브러리가 추가됩니다
- `script`: 컨트랙트를 관리하는 스크립트를 구현합니다. (배포 스크립트 등)
- `src`: 컨트랙트가 담깁니다
- `test`: 컨트랙트를 테스트하는 코드가 담깁니다.
### 기본 컨트랙트 보기
`src/Counter.sol`을 열어봅니다. 다음과 같은 코드를 볼 수 있습니다.
```solidity=
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Counter {
uint256 public number;
function setNumber(uint256 newNumber) public {
number = newNumber;
}
function increment() public {
number++;
}
}
```
솔리디티의 컨트랙트는, 하나의 전역 싱글톤 객체(Global Singleton Object)입니다.
즉, 별도의 저장 과정을 거치지 않아도 객체의 Attribute를 변경하면, 상태가 체인에 반영되게 됩니다.
또한, `number` 변수의 접근 제어자를 `public` 으로 설정함으로써, 별도의 `Getter` 함수를 선언하지 않고 해당 변수의 값에 접근할 수 있습니다.
## 컨트랙트 배포하기
### 로컬 넷 띄워보기
컨트랙트를 배포하기 위해서는 적절한 체인이 필요합니다. 기본적으로 테스트넷과, 메인넷 또한 불가능 한 건 아니지만, 실제 비용을 사용해야 한다거나, 느린 속도 때문에 기본 테스트의 경우에는 추천하지 않습니다.
이런 니즈가 있는 개발자들을 위해 다양한 로컬 이더리움 클라이언트들이 존재합니다. 그 중 가장 가볍고, 이미 설치되어 있는 `anvil`을 사용할 예정입니다.
새로운 터미널 창을 띄운 후, `anvil` 명령어를 실행합니다. 다음과 같은 화면이 뜨고, 맨 밑에 `Listening on 127.0.0.1:8545` 이라는 명령어가 뜬다면 로컬 노드가 정상적으로 작동한다는 의미입니다.
```
_ _
(_) | |
__ _ _ __ __ __ _ | |
/ _` | | '_ \ \ \ / / | | | |
| (_| | | | | | \ V / | | | |
\__,_| |_| |_| \_/ |_| |_|
0.1.0 (7278695 2022-08-27T00:28:29.9845Z)
https://github.com/foundry-rs/foundry
Available Accounts
==================
(0) 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
(1) 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 (10000 ETH)
(2) 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc (10000 ETH)
(3) 0x90f79bf6eb2c4f870365e785982e1f101e93b906 (10000 ETH)
(4) 0x15d34aaf54267db7d7c367839aaf71a00a2c6a65 (10000 ETH)
(5) 0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc (10000 ETH)
(6) 0x976ea74026e726554db657fa54763abd0c3a0aa9 (10000 ETH)
(7) 0x14dc79964da2c08b23698b3d3cc7ca32193d9955 (10000 ETH)
(8) 0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f (10000 ETH)
(9) 0xa0ee7a142d267c1f36714e4a8f75612f20a79720 (10000 ETH)
Private Keys
==================
(0) 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
(1) 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
(2) 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
(3) 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6
(4) 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a
(5) 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba
(6) 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e
(7) 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356
(8) 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97
(9) 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6
```
제대로 작동하는지 확인하기 위해, 첫번째 주소에서 두번째 주소로 1ETH를 옮겨 보겠습니다.
```shell
cast send 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 --value 1ether --from 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
```
정상적으로 작동했다면 첫번째 주소의 balance는 `9999ETH` 가 될 것이고, 두번째 주소의 balance는 `10001ETH`가 될 것입니다. 다음과 같은 명령어로 각 주소의 자산 가치를 파악할 수 있습니다.
```shell!
cast balance ${Address}
```
### 컨트랙트 배포하기
준비가 되었다면, 배포해 봅시다. 다음과 같은 명령어로 배포 가능합니다.
```shell
cast create --rpc-url http://127.0.0.1:8545/ \
--private-key ${your-private-key} \
src/Counter.sol:Counter
```
정상적으로 실행됐다면 다음과 같은 명령어가 나오게 됩니다.
```shell!
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Transaction hash: 0x966069179ddca56a6041bec47509d993a55854a4336e4c1298434eb95036209a
```
### 컨트랙트 테스트하기
컨트랙트의 함수들을 실행해보면서, 제대로 컨트랙트가 배포되었는지 확인해 봅시다.
**setNumber(uint256) 함수**
```
cast send ${contract-address} "setNumber(uint256)" "7" --from 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
```
**increase() 함수**
```
cast send ${contract-address} "increase()" --from 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
```
**number() 함수**
```
cast call ${contract-address} "number()(uint256)"
```
---
# 나만의 토큰 만들어보기
이더리움에서 토큰을 정의하는 가장 기본적은 방법은 ERC20 표준입니다. 해당 표준을 따르는 컨트랙트를 만들고, 배포해 봅시다.
우선 기본적인 ERC20 인터페이스를 살펴봅시다.
```solidity=
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
```
우리가 알고 있는 여러 함수들 transfer, allowance, Approval 등이 보입니다. 기본적으로 솔리디티의 이런 표준들 (ERC20, ERC721 등)은 덕타이핑의 개념을 따릅니다.
> **덕 타이핑 (Duck Typing)이란?**
> "오리처럼 걸어다니고, 오리처럼 울면 오리이다" 라는 문구에 기반한 타입 확인 법으로, **어떤 객체의 타입을 확인하기 위해서, 어떤 함수가 구현되어 있는지를 확인하는 방법** 입니다. 즉, 위에 정의된 ERC20 함수들을 모두 선언하고 있다면, 다른 어떤 함수가 더 선언되더라도 ERC20으로 여기는 방법입니다.
이런 함수를 일일히 다 구현하는 것 또한 방법이지만, 그러기에는 우리는 너무 할 일이 많습니다. 이런 우리들을 위해 이미 대부분의 필요한 코드들을 구현해 놓은 라이브러리들이 있습니다. 아래 명령어를 통해 라이브러리를 설치해 봅시다.
```shell
forge install transmissions11/solmate
forge remappings > remappings.txt # path 전부를 입력할 필요 없게 만들어줍니다
```
## 라이브러리 사용해서 토큰 만들기
solmate에서는 기본적으로 ERC20 토큰 컨트랙트를 제공합니다. 해당 컨트랙트를 import하여 토큰을 만들어 봅시다.
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "solmate/tokens/ERC20.sol";
contract ERC20Mintable is ERC20 {
constructor (string memory name, string memory code, uint8 decimal)
ERC20(name, code, decimal)
{
}
}
```
대부분의 코드는 solmate의 ERC20 컨트랙트에서 구현하니, 이렇게만 작성해 주어도 작동합니다.
여기서 생성자(Constructor)에 들어가는 매개 변수는 다음과 같습니다.
- `name`: 토큰의 이름 (e.g. Ethereum)
- `code`: 토큰의 심볼 (e.g. ETH)
- `decimal`: 토큰의 decimal
## Mint and Burn 가능하게 만들기
위에 적혀진 함수들을 보았을 때, 놀랍게도 Mint와 Burn 옵션이 없습니다. ERC20에서 Mint와 Burn은 직접 Balance 값을 수정함으로써 이루어지게 됩니다.
우리가 사용할 라이브러리에도 기본적으로 `_mint`와 `_burn` 함수가 존재하지만, private 함수이기 때문에 `_mint` / `_burn`을 외부에서 호출할 수 없습니다. 추후 만들 faucet에서 사용하기 위해 우선 Mint와 Burn을 외부에서 호출 가능하게 만들어봅시다.
함수를 정의하기에 앞서서, `ERC20` 라이브러리에서는 함수를 이렇게 정의하고 있습니다.
```solidity=
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;
// Cannot overflow because the sum of all user
// balances can't exceed the max uint256 value.
unchecked {
balanceOf[to] += amount;
}
emit Transfer(address(0), to, amount);
}
function _burn(address from, uint256 amount) internal virtual {
balanceOf[from] -= amount;
// Cannot underflow because a user's balance
// will never be larger than the total supply.
unchecked {
totalSupply -= amount;
}
emit Transfer(from, address(0), amount);
}
```
우선 이 함수를 그대로 호출해 줄 것이므로, 다음과 같이 컨트랙트를 수정해 봅시다.
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "solmate/tokens/ERC20.sol";
contract ERC20Mintable is ERC20 {
constructor (string memory name, string memory code, uint8 decimal)
ERC20(name, code, decimal)
{
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
function burn(address _to, uint256 _amount) public {
_burn(_to, _amount);
}
}
```
작성을 완료했다면, 컨트랙트를 배포해 봅시다. 이전 명령어와는 다르게 생성자에 매개변수가 포함되어 있으므로 `--constructor-args` 옵션과 함께 배포해야 합니다.
```shell!
forge create \
--rpc-url http://127.0.0.1:8545 \
--private-key ${PRIVATE-KEY} \
src/Token.sol:ERC20Mintable \
--constructor-args "${TOKEN-NAME}" "${SYMBOL}" 18
```
저는 테스트로 `MNTS` 토큰을 만들었습니다. 이제 특정 주소로 민팅을 해 봅시다.
```shell!
cast send ${TOKEN-ADDRESS} \
"mint(address,uint256)" \
0xB817527f6231Fe5bb4FaB55fe8FecAAC121bfbC7 100000000000000000000000 \
--from 0x70997970c51812dc3a010c7d01b50e0d17dc79c8
```
다음과 같이 민팅을 성공한 모습을 볼 수 있습니다.

### 미션
다음 세션 전까지 해보면 좋은 미션이 있습니다.
1. 토큰 burn해보기
2. 메타마스크에 토큰 추가해서 보기
3. 토큰을 만든 account만 mint / burn 할수 있도록 하기
솔리디티가 어렵다면 다음과 같은 강좌를 추천합니다.
https://www.inflearn.com/course/%EC%86%94%EB%A6%AC%EB%94%94%ED%8B%B0-%EC%8A%A4%EB%A7%88%ED%8A%B8-%EC%BB%A8%ED%8A%B8%EB%9E%99