이번 포스팅에서는 스마트 컨트랙트로 토큰(Token)을 발행하는 방법에 대해 다루고자 한다.
< 목차 >
- ERC-20
- Solidity 데이터 타입
- 인스턴스 생성
- 스마트 컨트랙트로 토큰 발행하기
1. ERC-20
ERC-20은 이더리움 블록체인 네트워크에서 정한 표준 토큰 스펙이다. 다시 말해, ERC-20은 이더리움 네트워크 상에서 발행되는 토큰의 표준을 뜻하며 스마트 컨트랙트를 통해 토큰을 발행하기 위해서는 정해놓은 규격에 맞춰 코드를 작성해야 함을 의미한다. 그렇다면 여기서 이야기하는 규격이라는 것은 어떤 것을 의미하는 것일까?
우리는 TypeScript에서 interface라는 것을 사용하여 객체의 형태를 지정해 주었다. interface는 객체의 모양을 본 뜬 내용이라고 할 수 있으며 토큰을 만들기 위해서도 이러한 interface가 기본적으로 탑재되어 있다고 생각하면 좋을 듯 하다. 즉, Solidity로 토큰 생성 코드를 작성할 때 interface와 같은 것이 존재해서 규격에 맞게끔 코드를 작성하게 된다. TypeScript라고 가정했을 시에는 대략 아래와 같은 느낌이라고 생각하면 된다.
// TypeScript로 가정했을 시
interface balance {
address: string;
amount: number;
}
interface token {
name: string;
symbol: string;
balances: balance[]; // Solidity에서는 mapping
}
위와 같은 형태의 interface 대로 Solidity 코드를 작성하면 그게 토큰이 된다. 이더리움 네트워크 상의 스마트 컨트랙트 안에 상태변수로 name, symbol, balances 등이 있다면 특정 화면(예를 들면 메타마스크)에서는 토큰이라고 화면에 보여주게끔 지정해 놓았기 때문이다. 다른 변수명으로 지정해도 동작은 똑같이 되겠지만 스마트 컨트랙트 내용을 전달했을 때 화면 상에서 토큰이라고 표현하지는 못할 것이다.
스마트 컨트랙트를 사용해 토큰을 만들기 위해서는 정해진 규격을 따라야 한다. 정해진 규격대로 만들어야만 하고 변수명 역시 정해진대로 만들어줘야 한다. 특정 변수에다가 특정 내용을 담으면 그 내용을 기반으로 토큰이 만들어지게 되는 것이다. name이라는 변수에 담긴 내용은 토큰의 이름이 되고 symbol이라는 변수에 담긴 내용은 토큰의 단위가 된다. balances에는 비트코인에서의 UTXO와 같은 내용이 담겨져 있게 되는데 계정 주소와 해당 계정에 남은 잔액을 객체 형태로 나타낸 것이 balances가 된다. 아래는 TypeScript로 표현해 본 balances인데 대략 이런 느낌으로 balances가 표현된다고 생각하면 된다.
// TypeScript로 표현한 balances
balances = [
{
address: '주소1',
amount: '남은 금액'
},
{
address: '주소2',
amount: '남은 금액'
},
{
address: '주소3',
amount: '남은 금액'
}
]
2. Solidity 데이터 타입
토큰을 만들기 위한 스마트 컨트랙트를 작성할 때 사용하게 되는 데이터 타입에 대해 알아보고자 한다. 우선 address 라는 데이터 타입에 대해 살펴보도록 하자. Solidity에는 address라는 데이터 타입이 존재하는데 address는 string을 다룰 수 있는 20 byte 짜리 데이터 타입이다. 이더리움 네트워크에서 사용하는 계정 혹은 주소는 40글자로 이루어진 20 byte 짜리 문자열이다. 이러한 계정 혹은 주소를 저장할 수 있는 데이터 타입이 address이며 string으로 20 byte를 저장할 수 있는 공간이다.
다음으로 알아볼 것은 mapping 이라는 데이터 타입이다. mapping( string => uint256 ) 과 같이 나타내며 이러한 mapping 데이터 타입은 JavaScript 적으로 표현했을 때 객체라고 볼 수 있다. mapping( ) 안에 존재하는 string => uint256 에서 앞부분은 객체의 속성명이 되고 뒷부분은 속성값이 된다. 즉, mapping( string => uint256 ) 은 다음과 같은 형태의 데이터를 갖는다고 볼 수 있다.
mapping(string => uint256)
{
"asdf" : 10
}
앞서 언급한 balances 와 같은 형태를 Solidity 에서는 address와 mapping을 사용해 다음과 같이 표현하게 된다.
mapping(address => uint256) public balances;
- 데이터 타입 : mapping(address => uint256)
- 접근 제한자 : public
- 변수명 : balances
mapping(address => uint256) 에 의해 어떠한 계정이 얼마만큼의 잔액을 가지고 있는지 표현할 수 있으며 public 접근 제한자를 사용했기 때문에 getter 함수가 만들어지게 된다.
{
"0x...1" : 1000,
"0x...2" : 1000,
"0x...3" : 1000,
"0x...4" : 1000,
}
그리고 위와 같은 형태로 balances가 구성되어 있다고 했을 때 balances["0x...1"] 과 같은 방법으로 계정의 잔액을 조회하는 것이 가능하다.
3. 인스턴스 생성
다음과 같은 스마트 컨트랙트가 작성되어 있다고 하자.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
contract SimpleStore {
uint256 public value;
// 생성자 함수
constructor(uint256 _value) {
value = _value;
}
}
constructor( ) 함수는 생성자 함수로써 인스턴스를 생성할 때 인자값을 넣어줄 수 있다. 스마트 컨트랙트에 대한 인스턴스는 딱 한번만 생성되는데 인스턴스가 생성되는 시점은 배포할 때 즉, 스마트 컨트랙트 내용이 블록에 담겨 이더리움 네트워크 상에 올라갈 때이다. 위와 같이 constructor( ) 함수에 매개변수가 존재할 때는 인스턴스가 생성되기 이전 즉, 배포하기 전에 인자값을 전달해줘야만 한다. truffle을 사용해 migration 파일을 생성하고 배포를 진행해 보도록 하자.
const SimpleStore = artifacts.require('SimpleStore');
module.exports = function (deployer) {
deployer.deploy(SimpleStore);
};
위와 같이 migration 파일을 작성하여 배포를 진행해보면 다음과 같은 에러가 발생한다.
인스턴스를 생성할 때 인자값을 전달해줘야 하는데 아무런 인자값을 전달하고 있지 않기 때문에 배포 단계에서 에러가 발생하게 된 것이다. 인스턴스 생성시 인자값을 전달하기 위해서는 truffle 기준으로 deployer.deploy( ) 의 두번째 인자값으로 constructor( ) 함수의 인자값을 전달해주면 된다.
const SimpleStore = artifacts.require('SimpleStore');
module.exports = function (deployer) {
deployer.deploy(SimpleStore, 10);
};
인자값을 전달했을 시 위와 같이 배포가 제대로 진행되는 것을 확인할 수 있다. 추가로 migration을 진행했을 때 생성되는 SimpleStore.json 파일 안의 deployedBytecode 내용과 truffle console에서 조회한 배포시 발생시킨 트랜잭션의 input 내용을 조회해 보면 다음과 같은 차이점을 발견할 수 있다.
인스턴스를 생성할 때(배포를 진행할 때) 인자값으로 10을 전달하였고 전달한 인자값에 대한 내용이 input 데이터 안에 바이트 코드와 함께 들어가게 되는 것을 알 수 있다. 배포를 진행할 때 스마트 컨트랙트 안의 constructor( ) 함수가 실행되며 constructor( ) 함수가 인자값을 받고 있기 때문에 인자값에 대한 내용 역시 input 데이터 안에 포함되어 나타나게 된 것이다. 스마트 컨트랙트에서는 이와 같이 함수에 인자값이 있다면 0을 기준으로 구분값을 줘서 전달한 인자값을 표현하게 된다.
다음으로 알아볼 것은 msg.sender 라는 예약어이다. Solidity에는 이더리움 네트워크 안에서만 사용할 수 있는 변수가 있다. msg.sender가 그 중 하나인데 msg.sender를 통해 스마트 컨트랙트를 실행한 사람의 account를 가져올 수 있다. 위에서 작성한 SimpleStore 컨트랙트의 constructor( ) 함수에 다음과 같은 내용을 추가하여 보도록 하자.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
contract SimpleStore {
uint256 public value;
address public owner;
// 생성자 함수
constructor(uint256 _value) {
value = _value;
owner = msg.sender; // 배포시에는 배포를 실행한 사람의 EOA.
}
// 이후에는 컨트랙트를 실행시킨 사람의 계정(EOA)
}
constructor( ) 함수 안에서 owner 변수에 msg.sender 를 할당해 줄 경우 스마트 컨트랙트를 배포 시 배포를 실행한 EOA 가 owner 변수에 들어가게 된다. 그리고 인스턴스가 생성된 이후에는 스마트 컨트랙트를 실행한 EOA가 msg.sender에 들어가게 된다. truffle migration으로 배포를 진행한 다음 truffle console 에서 getter 함수를 통해 owner의 값을 조회해보면 배포를 진행한 EOA 값을 갖는 것을 확인할 수 있다.
4. 스마트 컨트랙트로 토큰 발행하기
앞서 언급했던 모든 내용들을 종합하여 실제 스마트 컨트랙트로 토큰을 발행하는 코드를 작성해보고자 한다. "ingToken"이라는 토큰을 생성하는 스마트 컨트랙트 코드를 다음과 같이 작성하였다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
contract ingToken {
mapping(address => uint256) public balances; // 속성명이 address, 속성값이 uint256인 객체
// 값이 없을 경우 undefined 나 에러나 나는 것이 아니라 디폴트 값인 0이 나온다.
// 상태변수의 이름은 규격을 맞춰줘야 한다.
string public name = 'ingToken';
string public symbol = 'ITK';
uint8 public decimals = 18;
uint256 public totalSupply = 10000000000 * 10 ** decimals;
constructor() {
balances[msg.sender] = totalSupply; // 배포를 진행한 EOA에게 총 발행량 지급
// constructor() 함수 안에서는 실행시킨 사람이 곧 배포한 사람이다.
}
function balanceOf(address _owner) public view returns(uint256 balance){
// 메타마스크에서 자동으로 연결된 계정을 balanceOf() 함수의 인자값으로 전달한다.
return balances[_owner];
}
// transfer() 함수가 있어야만 계정 간 토큰 전송이 가능하다.
function transfer(address _to, uint256 _value) public returns(bool success) {
// require(true) : 실행
// require(false) : 종료
require(balances[msg.sender] >= _value);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
}
앞서 언급했던 것처럼 스마트 컨트랙트를 통해 토큰을 발행하기 위해서는 정해놓은 규격에 맞춰 코드를 작성해야 한다. 변수명 뿐만 아니라 함수명 역시 정해져 있는 이름을 사용하여야 한다. 위에 작성된 ingToken 컨트랙트 안에서 각각의 변수와 함수들은 다음과 같은 기능을 한다.
- string public name : 토큰의 이름
- string public symbol : 토큰의 단위
- uint8 public decimals : 소수점 자릿수
- uint256 public totalSupply : 토큰의 총 발행량
- mapping( address => uint256 ) public balances : 토큰을 소유한 계정의 잔액들.
name, symbol, totalSupply, balances 모두 접근 제한자가 public 이기 때문에 getter 함수가 만들어지며 call( ) 함수를 통해 조회하는 것이 가능하다.
constructor() {
balances[msg.sender] = totalSupply; // 배포를 진행한 EOA에게 총 발행량 지급
// constructor() 함수 안에서는 실행시킨 사람이 곧 배포한 사람이다.
}
constructor( ) 함수 안에서 balances[ msg.sender ] = totalSupply 와 같이 작성된 것을 볼 수 있는데 constructor( ) 함수 안에서 msg.sender는 스마트 컨트랙트를 배포한 계정을 의미한다. 이는 결국 배포를 진행한 EOA 에게 토큰의 총 발행량을 지급하겠다는 의미가 된다.
function balanceOf(address _owner) public view returns(uint256 balance){
return balances[_owner];
}
balanceOf( ) 함수는 계정의 잔액을 조회하는 함수이다. 해당 함수를 통해 인자값으로 전달받은 계정의 잔액을 조회할 수 있다. balanceOf( ) 함수는 해당 함수를 실행시킨 계정 뿐만 아니라 다른 계정의 잔액을 조회하는데도 사용되어야 하기 떄문에 계정 정보를 인자값으로 받는 형태로 규격화 되어 있다. 일례로 메타마스크에서는 자동으로 연결된 계정을 balanceOf( ) 함수의 인자값으로 전달하여 해당 계정의 토큰 잔액을 조회할 수 있게끔 한다.
function transfer(address _to, uint256 _value) public returns(bool success) {
// require(true) : 실행
// require(false) : 종료
require(balances[msg.sender] >= _value);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
transfer( ) 함수는 계정 간 토큰 전송이 가능하게끔 하는 함수이다. 스마트 컨트랙트 안에 transfer( ) 함수가 정의되어 있어야만 계정 간 토큰 전송이 가능해진다. transfer( ) 함수는 call 함수가 아닌 send 함수로 transfer( ) 함수를 실행하기 위해서는 트랜잭션을 발생시켜야만 한다. 그리고 인자값으로는 누구에게 전송할 것인지에 대한 address _to 와 보낼 금액에 해당하는 uint256 _value 를 받는다.
transfer( ) 함수 안에 작성된 require( balances[msg.sender] >= _value ) 구문은 Solidity에서 마치 조건문처럼 동작하는 코드이다. require( ) 함수의 인자값이 true 값일 경우에 한해서 함수가 실행되며 인자값이 false 값일 경우에는 함수 실행이 종료된다. 따라서 transfer( ) 함수는 require( ) 함수에 의해 트랜잭션을 발생시킨 계정의 잔액( balances[msg.sender] )이 보낼 금액(_value) 보다 크거나 같을 때만 실행된다. transfer( ) 함수가 정상적으로 실행됐다면 balances 안에 있는 msg.sender 계정의 잔액은 _value 만큼 차감하고 _to 에 해당하는 계정의 잔액은 _value 만큼 가산해주면 된다.
'Ethereum' 카테고리의 다른 글
Ethereum/이더리움 - Solidity(솔리디티) function payable (0) | 2022.07.21 |
---|---|
Ethereum/이더리움 - 스마트 컨트랙트로 투표 Dapp 만들기 (0) | 2022.07.18 |
Ethereum/이더리움 - 스마트 컨트랙트 이벤트 등록 및 백엔드에서 트랜잭션 생성하기 (0) | 2022.07.14 |
Ethereum/이더리움 - 메타마스크를 통한 스마트 컨트랙트 실행 (0) | 2022.07.13 |
Ethereum/이더리움 - Truffle (스마트 컨트랙트 개발 프레임워크) (0) | 2022.07.13 |