이번 포스팅에서는 토큰을 발행하는 스마트 컨트랙트 코드를 작성하기 위한 전 단계로 인터페이스와 ERC-20에 대해 알아보고자 한다. 그리고 ERC-20을 토대로 실제 토큰 발행까지 진행해보도록 하자.
< 목차 >
- 인터페이스 (interface)
- ERC-20 컨트랙트
- JwToken 발행하기
스마트 컨트랙트를 통해 토큰을 발행하기 위해서는 이더리움 네트워크 상에서 발행되는 토큰의 표준, 규격을 가리키는 ERC-20 에 맞춰 코드를 작성해줘야만 한다. 쉽게 말해, 이더리움이 정한 표준 토큰 스펙인 ERC-20에 맞춰 토큰을 발행한다고 생각하면 된다. ERC-20에 맞춰 스마트 컨트랙트를 작성하기에 앞서 Solidity에서 사용되는 interface에 대해 언급하고 넘어갈 필요성이 있다.
1. 인터페이스 (interface)
이전 포스팅에서 TypeScript의 interface에 비유하면서 토큰 발행과 관련된 규격에 대해 언급한 적이 있다.
이전 글)
2022.07.17 - [Ethereum] - Ethereum/이더리움 - 스마트 컨트랙트로 토큰 발행하기
Solidity(솔리디티)에도 TypeScript와 마찬가지로 interface가 존재한다. Solidity에서는 하나의 컨트랙트가 또 다른 컨트랙트와 상호작용하기 위해 interface가 사용된다. interface는 자식 컨트랙트를 위한 하나의 틀이라고 볼 수 있으며 다음과 같은 특징을 갖는다.
- interface에서는 함수의 기능은 정의하지 않는다.
- 함수의 이름, 입력 매개변수, return 값만 선언하고 함수의 내용은 없는 추상 함수로만 interface를 구성해야 한다. 함수의 내용은 해당 interface를 상속받는 컨트랙트 쪽에서 구현하게 된다.
- 다른 컨트랙트들과 상호작용 할 때 해당 interface를 상속 받은 컨트랙트는 이런 함수들을 포함하고 있다고 미리 정보를 주는 역할을 한다.
- 다른 interface에서 상속 가능하다.
- interface에서 선언된 함수들은 무조건 external 타입이어야 한다.
- interface에서는 생성자 함수(constructor)를 선언할 수 없다.
- interface에서는 상태 변수를 선언할 수 없다.
Solidity에서 사용되는 interface에 대해 대략적으로 정리해 본 것인데 실제 ERC-20에 사용되는 interface를 다음과 같이 만들어 보았다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
// ERC20에 사용되는 인터페이스
interface IERC20 {
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address recipient, uint amount) external returns (bool);
// 위임 받은 돈을 관리하는 공간 (데이터 저장 공간)
function allowance(address owner, address spender) external returns (uint);
// 위임장 (제 3자가 돈을 사용할지 말지 허락하는 함수)
function approve(address spender, uint amount) external returns (bool);
// 관리하는 돈을 보내는 함수
function transferFrom(address spender, address recipient, uint amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
}
interface에서는 위에서 보이는 바와 같이 함수의 기능에 관한 내용 없이 추상함수 형태로만 작성하게 된다. 그리고 선언되는 함수들은 external 타입이어야 함에 주의하도록 하자. 각각의 함수가 하는 역할은 대략적으로 다음과 같다.
- totalSupply( ) : 토큰의 총 발행량을 조회하는 veiw 함수.
- balanceOf( ) : 특정 계정의 토큰 잔액을 조회하는 view 함수.
- transfer( ) : 계정 간 토큰 전송에 사용되는 함수. transfer( ) 함수를 통해 트랜잭션을 발생시키게 되며 인자값으로 누구에게(address recipient) 얼마(uint amount)를 전송할지를 받는다.
- allowance( ) : 계정 소유자가 CA 혹은 다른 계정에게 토큰 사용 권한을 위임할 수 있는데 어떤 계정에게 얼마만큼을 위임했는지 조회할 수 있는 함수이다. 실제로 IERC를 상속받는 컨트랙트에서 mapping 데이터 타입으로 allowance 상태변수를 만들어 줄 것이다.
- approve( ) : 앞서 언급한 위임과 관련된 함수이다. approve( ) 함수를 이용해 계정 소유자가 CA 혹은 다른 계정에게 얼마만큼의 토큰을 위임할 것인지 결정할 수 있다.
- transferFrom( ) : 권한을 위임받은 계정은 transferFrom( ) 함수를 사용해 위임을 부여한 계정의 토큰을 전송할 수 있다.
- event Transfer( ) : 토큰 전송이 일어났을 때 발동되는 이벤트이다. value 값이 0일 때에도 이벤트는 발생된다.
- event Approval( ) : approve( ) 함수가 성공적으로 실행되고 난 후에 발동하는 이벤트이다.
이제 ERC20의 인터페이스인 IERC20을 상속받는 ERC20 컨트랙트 코드를 작성해보면서 각각의 함수들의 기능 및 제 3자간의 거래와 관련된 함수들에 대해 좀 더 이해해보도록 하자. ( 여기서 제 3자 간의 거래와 관련된 함수는 allowance , approve , transferFrom이 있다. )
2. ERC-20 컨트랙트
위에서 IERC20 이라는 ERC20에서 사용하게 될 인터페이스를 만들어주었다. 이제 ERC20 컨트랙트를 작성할 때 인터페이스를 상속받은 후 작성을 진행해주면된다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import './IERC20.sol';
// interface 상속 받아서 함수 기능 구현
contract ERC20 is IERC20 {
string public name;
string public symbol;
uint8 public decimals = 18;
uint public override totalSupply;
mapping(address => uint) public balances;
mapping(address => mapping(address => uint)) public override allowance;
}
우선 IERC20을 상속받는 방법부터 짚고 넘어가도록 하자. Solidity에서는 import './IERC20.sol' 구문을 통해 interface IERC20 이 작성되어 있는 파일 전체의 코드를 가져오게 된다. 그리고 IERC20을 상속 받는 ERC20 컨트랙트를 작성하기 위해서는 다음과 같이 is 구문을 사용한다.
contract ERC20 is IERC20 { }
ERC20 컨트랙트 안에서 name, symbol, decimals, totalSupply, balances, allowance 상태변수를 만들어 주었는데 totalSupply와 allowance에 override 라는 키워드가 붙어있는 것을 알 수 있다. 이는 interface IERC에 function totalSupply와 function allowance가 선언되어 있기 때문인데 이와 같이 인터페이스로부터 상속받은 함수들 앞에는 override 키워드를 붙여준다.
override 속성은 쉽게 말해 상속받은 함수를 덮어쓰기 하겠다는 뜻이다. 부모 컨트랙트의 함수가 virtual 속성을 가지고 있을 경우 자식 컨트랙트는 상속 받은 함수 앞에 override 속성을 달아줘야 한다. 하지만 interface 내의 모든 함수들은 virtual 함수이기 때문에 현재 interface IERC20로부터 상속 받은 함수들에는 override 키워드를 붙여줘야만 한다.
다음으로는 allowance 상태변수에 대해 짚고 넘어가자. allowance에 대해 이해하기 위해서는 ECR20 스마트 컨트랙트 안에서 구현되는 "위임"이라는 시스템에 대해 이해해야만 한다. 가령 A , B , C 세 사람이 존재한다고 하자. A 는 자신의 계정에 존재하는 특정 금액만큼의 사용 권한을 B에게 위임할 수 있다. A로부터 위임 권한을 부여 받은 B는 A 계정에 있는 토큰을 제 3자인 C에게 전송할 수 있게 된다. 그리고 이러한 위임 관계는 allowance 라는 상태변수 안에 명시되어 있다.
실제 이더리움 공식문서에서도 ERC20 토큰이 제공하는 기능을 다음과 같이 명시하고 있다.
- transfer tokens
- allow others to transfer tokens on behalf of the token holder
두번째 기능에 해당하는 "다른 사람이 토큰 소유자를 대신하여 토큰을 전송하도록 허용"하는 것이 바로 allowance 안에 명시되어 있다고 생각하면 된다.
allowance의 데이터 타입을 살펴보면 mapping 안에 mapping이 존재하는 형태인 것을 확인할 수 있다.
mapping(address => mapping(address => uint)) public override allowance;
mapping을 이중으로 사용하고 있긴 하지만, 다음과 같이 객체 형태로 풀어서 생각하면 그리 어렵지 않게 그 의미를 파악할 수 있다.
// 객체 형태로 나타내 본 allowance 형태
const allowance = {
"0x3B8d04BcAd10B641Ec1a2C48DD51606949EDd5fC": {
"0xF1BB833D8827B53c7EFc0F25e1D9FC4C57BF28C5": 100,
"0x23f5Fc49cc2ff78E54d88f2F213db8cFD9415d2a": 200
},
"0xad3e55e0289c93082B3c8c2b7a55E20A9f9e0A8C": {
"0xF1BB833D8827B53c7EFc0F25e1D9FC4C57BF28C5": 300,
"0x23f5Fc49cc2ff78E54d88f2F213db8cFD9415d2a": 400,
"0x58A864C2384467726F33adc8200706Dd261E53EB": 500
}
}
해석해보면 "0x3B8d..."(account 0) 계정 소유자가 자신이 가지고 있는 토큰들 중 100 토큰에 해당하는 양만큼 "0xF1BB..."(account 1) 계정에게 위임했으며 "0x23f5..."(account 2) 계정에게는 200 토큰에 해당하는 양만큼 권한을 위임했다고 볼 수 있다. account 0으로 부터 100 토큰을 위임 받은 account 1은 transferFrom( ) 함수를 통해 100 토큰을 넘지 않는 선에서 account 0의 토큰을 제 3자에게 전송하는 것이 가능해진다.
totalSupply 와 balances에 대한 설명은 이전 포스팅에서부터 계속해서 다뤄왔던 내용이므로 생략하도록 하고 ERC20 컨트랙트 안에서 만들어줄 함수들을 살펴보도록 하자.
// 계정 잔액 조회 함수
function balanceOf(address account) view override external returns (uint) {
return balances[account];
}
// 토큰 전송 함수
function transfer(address recipient, uint amount) external override returns (bool) {
balances[msg.sender] -= amount;
balances[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
balanceOf( ) 함수는 특정 계정의 잔액을 조회하는 함수이며 transfer( ) 함수는 토큰 전송시 사용되는 함수이다. 계속해서 언급해왔던 함수들이므로 자세한 설명은 생략하고자 한다. 주의할 점은 두 함수 모두 interface IERC20에서 선언해준 함수이기 때문에 override 속성을 붙여줘야 한다는 것이다.
참고로 internal 과 external 속성에 대해 간략히 언급하고 넘어가면, internal 은 private 접근 제한자와 유사한 기능을 하며 상속받는 컨트랙트에서는 접근이 가능하다. 반대로 external 은 public 접근 제한자와 유사하며 external 키워드가 붙은 함수는 오직 컨트랙트 외부에서만 호출이 가능하다. 다시말해, 컨트랙트 내부의 다른 함수들에서는 external 키워드가 붙은 함수를 호출할 수 없게 된다.
다음으로는 위임 방식의 거래에 활용되는 함수들을 만들어주고자 한다.
// 위임 권한을 주는 함수
function approve(address spender, uint amount) external override returns (bool){
allowance[msg.sender][spender] = amount; // msg.sender가 spender에게 위임 권한을 주는 코드
emit Approval(msg.sender, spender, amount); // 로그 기록 / approve 성공시 발생되는 이벤트
return true;
}
approve( ) 함수를 통해 계정 소유자는 CA (contract address) 혹은 특정 계정에게 일정 금액 만큼을 위임할 수 있게 된다. allowance[ msg.sender ][ spender ] = amount 에 의해 amount에 해당하는 금액만큼 spender에게 위임하게 되며 여기서 msg.sender는 approve( ) 함수를 호출한 주체이다. 즉, approve( ) 함수를 호출한 주체인 msg.sender가 spender에게 amount 만큼의 토큰을 위임해주는 함수가 approve( ) 함수 자체인 것이다. 그리고 위임에 성공했다면 Approval( ) 이벤트를 발생시켜 위임 관련 내용을 로그에 기록하게 된다.
다음은 transferFrom( ) 함수이다.
function transferFrom(address sender, address recipient, uint amount) external override returns (bool) {
require(allowance[sender][msg.sender] >= amount);
allowance[sender][msg.sender] -= amount;
balances[sender] -= amount;
balances[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}
transferFrom( ) 함수를 호출하는 주체는 위임 권한을 부여받은 계정 소유자가 된다. 따라서 해당 함수 안에서 sender와 msg.sender가 각각 어떤 계정을 지칭하는지에 대해 정확히 구분하고 넘어가야 한다.
- sender : 전송할 토큰의 실소유자 계정.
- msg.sender : transferFrom( ) 함수를 호출한 계정으로 특정 계정(sender)으로부터 일정량의 토큰을 위임 받은 계정.
approve( ) 함수 안의 allowance[ msg.sender ][ spender ] 와 transferFrom( ) 함수 안의 allowance[ sender ][ msg.sender ] 를 비교해보면 다음과 같다.
- approve( ) 함수 안에서 msg.sender == transferFrom( ) 함수 안에서 sender
- approve( ) 함수 안에서 spender == transferFrom( ) 함수 안에서 msg.sender
정리하면, A가 B에게 일정량의 토큰에 대한 위임 권한을 부여할 경우 approve( ) 함수를 호출하는 주체인 msg.sender는 A 계정이 되고 approve( ) 함수 안에서의 spender는 B 계정이 된다. B가 제 3자인 C에게 A 계정의 토큰을 전송하고 싶은 경우 B는 transferFrom( ) 함수를 호출하게 되는데 이 때 transferFrom( ) 함수 안에서의 sender는 A 계정이 되고 transferFrom( ) 함수를 호출하는 주체인 msg.sender는 B 계정이 된다. 그리고 C 계정은 recipient 가 되어 B가 전송한 A 계정의 토큰을 전달받는다.
approve( ) 함수와 transferFrom( ) 함수 모두 interface IERC20에서 선언해주었던 함수이기 때문에 override 키워드를 붙여주었으며 external 함수로 선언하였다. 그리고 transferFrom( ) 함수의 경우 require( ) 구문을 사용해 위임 받은 토큰의 양이 amount 보다 클 경우에만 트랜잭션을 발생시킬 수 있도록 하였고 전송이 완료되었을 시 컨트랙트 실행 공유를 위해 Transfer( ) 이벤트를 발생시켰다.
마지막으로 만들어줄 함수는 mint( ) 함수와 burn( ) 함수인데 각각의 내용은 다음과 같다.
// mint (토큰 수령)
function mint(uint amount) internal {
balances[msg.sender] += amount;
totalSupply += amount;
emit Transfer(address(0), msg.sender, amount);
}
// 총 발행량 지우는 함수 (소각)
// address(0) == address 타입으로 null값 부여
function burn(uint amount) external {
balances[msg.sender] -= amount;
totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
두 함수 모두 interface IERC20으로부터 상속받은 함수는 아니기 때문에 override 속성을 붙여주지 않았다. mint( ) 함수는 토큰 발행 및 수령과 관련된 함수로써 토큰 발행에 있어 엔트리포인트와도 같은 함수이다. mint( ) 함수를 호출함으로써 특정 계정에 얼마만큼의 토큰을 발행해 줄 것인지 정할 수 있으며 만약 토큰 스마트 컨트랙트의 constructor( ) 함수 안에서 mint( ) 함수가 호출될 경우에는 스마트 컨트랙트를 배포한 EOA 가 msg.sender가 되어 해당 계정이 최초 발행된 물량만큼의 토큰을 수령하게 된다. 이와 동시에 토큰의 총 발행량인 totalSupply 역시 발행된 토큰만큼 그 값을 증가시켜줘야 한다. 그리고 mint( ) 함수를 통한 토큰의 발행은 특정 계정에서 토큰이 빠져나가는 개념이 아니기 때문에 mint( ) 함수 안에서 호출되는 Transfer( ) 이벤트의 경우에도 from 값이 없기 때문에 null 값을 의미하는 address(0)을 인자값으로 넣어주게 된다.
burn( ) 함수는 mint( ) 함수와는 반대로 토큰의 소각과 관련된 함수이다. burn( ) 함수를 호출한 계정의 토큰을 amount 만큼 소각시키며 이 때 총 발행량인 totalSupply에서도 소각된 양만큼 그 값을 빼주게 된다. 그리고 burn( ) 함수 안에서 호출되는 Transfer( ) 이벤트 역시 to 값이 없기 때문에 address(0)을 인자값으로 전달해주게 된다.
다음은 지금까지 작성한 ERC20 컨트랙트의 전체 코드이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import './IERC20.sol';
contract ERC20 is IERC20 {
string public name;
string public symbol;
uint8 public decimals = 18;
uint public override totalSupply;
mapping(address => uint) public balances;
mapping(address => mapping(address => uint)) public override allowance;
function balanceOf(address account) view override external returns (uint) {
return balances[account];
}
function transfer(address recipient, uint amount) external override returns (bool) {
balances[msg.sender] -= amount;
balances[recipient] += amount;
emit Transfer(msg.sender, recipient, amount);
return true;
}
function approve(address spender, uint amount) external override returns (bool){
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address sender, address recipient, uint amount) external override returns (bool) {
require(allowance[sender][msg.sender] >= amount);
allowance[sender][msg.sender] -= amount;
balances[sender] -= amount;
balances[recipient] += amount;
emit Transfer(sender, recipient, amount);
return true;
}
function mint(uint amount) internal {
balances[msg.sender] += amount;
totalSupply += amount;
emit Transfer(address(0), msg.sender, amount);
}
function burn(uint amount) external {
balances[msg.sender] -= amount;
totalSupply -= amount;
emit Transfer(msg.sender, address(0), amount);
}
}
3. JwToken 발행하기
위에서 작성한 ERC20 컨트랙트를 기반으로 JwToken 컨트랙트를 작성해 토큰을 발행해보고자 한다.
우선 JwToken 컨트랙트의 전체 코드는 다음과 같으며 세부 내용을 하나씩 살펴보도록 하자.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import './ERC20.sol';
contract JwToken is ERC20 {
address public owner;
uint256 public ethCanBuy = 200;
constructor(string memory _name, string memory _symbol, uint256 _amount) {
owner = msg.sender;
name = _name;
symbol = _symbol;
mint(_amount * (10 ** uint256(decimals)));
}
// 익명함수
receive() external payable {
require(msg.value != 0);
uint amount = msg.value * ethCanBuy;
require(balances[owner] >= amount);
balances[owner] -= amount;
balances[msg.sender] += amount;
if (msg.sender == owner) {
mint(amount);
}
emit Transfer(owner, msg.sender, amount);
}
}
JwToken을 발행하는 컨트랙트는 앞서 작성한 ERC20 컨트랙트를 기반으로 작성되었다. 이렇게 특정 규격에 맞춰 토큰을 발행하기 위해 ERC20 컨트랙트를 미리 작성해 놓은 다음 ERC20을 상속 받아 여러개의 토큰 컨트랙트를 작성하는 것이 가능하다.
import './ERC20.sol';
// ERC20 컨트랙트를 상속받아 JwToken 컨트랙트 작성
contract JwToken is ERC20 { }
다음으로 JwToken 컨트랙트 안에서 owner 상태변수와 ethCanBuy 상태변수를 추가로 만들어 주었는데 해당 상태변수에 대한 설명을 위해서 JwToken 컨트랙트 안에서 어떠한 기능을 구현할 것인가에 대해 먼저 언급해보고자 한다.
우선 JwToken 컨트랙트를 배포함과 동시해 배포한 EOA로 일정량의 JwToken이 발행되도록 만들고자 한다. 이 때 JwToken 컨트랙트의 배포자는 owner 상태변수에 할당된다. 즉, JwToken 컨트랙트의 배포자가 곧 토큰의 발행자인 셈이다. 그리고 JwToken 컨트랙트의 CA (contract address)로 이더를 전송했을 때 일정 비율을 가지고 이더를 토큰으로 스왑해주는 기능을 구현하고자 한다. 이때 사용되는 상태 변수가 바로 ethCanBuy 변수이다. ethCanBuy 를 200으로 설정해 놓은 상태이므로 1 ETH를 전송할 경우 200개의 토큰을 돌려받을 수 있는 셈이다.
constructor(string memory _name, string memory _symbol, uint256 _amount) {
owner = msg.sender;
name = _name;
symbol = _symbol;
mint(_amount * (10 ** uint256(decimals)));
}
JwToken 컨트랙트의 constructor( ) 함수를 살펴보면 위와 같이 _name , _symbol , _amount 를 인자값으로 받고 있는 것을 확인할 수 있다. JwToken 컨트랙트를 이더리움 네트워크 상에 배포할 시 토큰의 이름, 단위, 그리고 최초 발행량을 인자값으로 전달해주면서 배포하겠다는 의미로 받아들이면 된다. 그리고 ERC20 컨트랙트로부터 상속받은 mint( ) 함수를 constructor( ) 함수 안에서 호출시켜 스마트 컨트랙트를 배포함과 동시에 JwToken 컨트랙트 배포자의 EOA로 _amount * (10 ** uint256(decimals)) 만큼의 토큰이 발행되도록 하였다.
정리하면, constructor( ) 함수 안에서 owner = msg.sender 이기 때문에 JwToken 컨트랙트 배포자는 owner가 되며 constructor( ) 함수 안에서 mint( ) 함수가 호출되었기 때문에 스마트 컨트랙트 배포와 동시에 토큰이 발행되고 owner의 EOA로 최초 발행량 만큼의 토큰이 지급된다.
참고로 mint( ) 함수의 인자값을 전달할 때 _amount * (10 ** uint256(decimals)) 형태로 최초 발행 수량을 전달한 데에는 다음의 이유가 있다.
- 이더리움 네트워크 상에서의 거래는 wei 단위로 계산되기 때문에 계산의 편의를 위해 1 token == 1 wei 로 단위를 맞춰주기 위해 _amount(발행량)에 10^18을 곱해주었다. (여기서 decimals == 18)
- ERC20 컨트랙트에서는 decimals 가 uint8 타입으로 선언되어 있지만 mint( ) 함수는 인자값으로 uint256 타입의 값을 받는다. 따라서 uint256(decimals)를 통해 형 변환을 진행해주었다.
다음으로 살펴볼 것은 receive( ) 함수인데 해당 함수로 인해 JwToken 컨트랙트의 CA로 이더를 전송했을 때 ethCanBuy에 해당하는 비율만큼 이더에서 토큰으로 스왑이 가능해진다. receive( ) 함수 안에서 이러한 기능을 구현할 수 있는 이유는 receive( ) 함수가 익명함수이기 때문이다.
👉 익명함수 - receive( ) & fallback( )
Solidity에서의 익명함수에 대해 잠시 짚고 넘어가고자 한다. 우선 익명함수의 종류에는 receive( ) 함수와 fallback( ) 함수가 있다. 원래는 fallback( ) 함수 하나만 존재했었는데 기능이 세분화 되면서 receive( ) 함수와 fallback( ) 함수로 나뉘어졌다고 보면 된다.
익명함수는 트랜잭션 data 속성 안에 실행하고자 하는 스마트 컨트랙트 함수가 없을 때 자동으로 실행되는 함수를 일컫는다. 트랜잭션 안에 실행시킬 함수가 정해져 있지 않을 때 즉, 스마트 컨트랙트 상의 함수를 호출하는 트랜잭션이 아닐 경우에 익명함수가 자동으로 실행된다. 이더리움 네트워크 상에서 발생하는 트랜잭션의 종류에는 단순히 이더 전송을 위한 트랜잭션과 스마트 컨트랙트 코드 실행 목적의 트랜잭션이 있다. 여기에 익명함수라는 것을 만들어서 컨트랙트 코드 실행이 아닌 단순한 코인 전송의 트랜잭션일 경우에도 자동으로 실행되는 함수가 존재하게끔 한 것이라고 보면 된다.
fallback( ) 함수와 receive( ) 함수의 기능은 각각 다음과 같이 세분화 되었다고 볼 수 있다.
- fallback( ) : 특정 계정에서 스마트 컨트랙트 코드를 실행시키기 위해 트랜잭션을 발생시켰는데 실행시키고자 하는 함수가 스마트 컨트랙트 내에 존재하지 않을 때 fallback( ) 함수가 실행된다. fallback( ) 함수의 경우 트랜잭션 안에 value 값이 있든 없든 실행되며 payable 속성 역시 옵션값으로 들어간다. 다시말해, 컨트랙트 안에 호출되는 함수가 없을 때 실행되는 함수가 fallback( ) 함수이다.
- receive( ) : 발생된 트랜잭션 안에 호출시키고자 하는 함수가 없을 때 즉, data 속성값이 없을 때 실행되는 함수가 receive( ) 함수이다. receive( ) 함수의 경우 트랜잭션 안에 value 값이 있을 때만 실행되며 payable 속성이 필수이다. 즉, 특정 계정에서 CA (contract address)에 이더를 전송했을 때 실행되는 함수가 receive( ) 함수라고 생각하면 된다.
통상적으로 이더/토큰과 관련된 내용의 코드들은 receive( ) 함수를 사용해 처리하고 잘못된 호출에 관한 처리는 fallback( ) 함수 안에서 구현한다고 보면 된다. 여기서는 receive( ) 함수를 사용해서 특정 계정이 JwToken 컨트랙트의 CA로 이더를 전송했을 때 토큰으로 교환해주는 코드를 작성해 보았다.
receive() external payable {
require(msg.value != 0);
uint amount = msg.value * ethCanBuy; // 1 ETH 당 200 token
require(balances[owner] >= amount);
balances[owner] -= amount;
balances[msg.sender] += amount;
// CA로 이더를 전송한 계정이 발행자 계정일 경우 토큰 발행량을 늘려주는 코드
if (msg.sender == owner) {
mint(amount);
}
emit Transfer(owner, msg.sender, amount);
}
우선 require( msg.value != 0 ); 구문을 사용해 0 이더를 전송했을 시에는 receive( ) 함수가 실행되지 않도록 조건을 걸어주었다. 현재 ethCanBuy에 할당된 값은 200 이므로 1 이더(ether) 당 200개의 토큰으로 스왑 비율이 형성되고 amount 변수에 스왑할 토큰의 개수를 만들어서 할당해주게 된다.
이더를 토큰으로 스왑하고자 할 때 JwToken의 CA를 향해 이더를 전송하므로 이더를 보관하게 되는 주체는 JwToken 컨트랙트의 CA 이다. 하지만 토큰 자체는 owner 즉, 토큰 발행자로부터 지불이 이루어진다. 따라서 스왑하고자 하는 토큰 전체의 양(amount)이 balances[owner] 의 값보다 작거나 같을 때만 receive( ) 함수가 실행되어 스왑이 이루어져야 한다. require(balances[owner] >= amount) 조건을 걸어주었으며 토큰 스왑이 성공적으로 이뤄지면 balances[owner] 에서 amount 만큼 토큰을 차감하고 balances[msg.sender] 에는 amount 만큼 토큰의 양을 증가시켜주었다.
그리고 if 문을 통해 토큰 발행자가 자신이 배포한 JwToken 컨트랙트의 CA를 향해 이더를 전송할 경우 mint( ) 함수를 실행시켜 전체 토큰 발행량을 증가시키는 코드를 추가해주었다. 마지막으로 토큰 스왑이 완료되었을 시 Transfer( ) 이벤트를 호출해주었다. 이 때 앞서 언급했던 것처럼 토큰을 지불하는 계정은 owner 계정이므로 Transfer( ) 이벤트의 인자값으로 들어가는 from 값은 owner , to 는 msg.sender가 된다.
실제 JwToken을 배포하여 메타마스크를 통해 토큰 스왑이 이루어지는지 확인해보도록 하자. ganache-cli 를 이용해 로컬 이더리움 네트워크 상에 배포를 진행하였으며 배포는 truffle을 사용하여 진행해주었다. 아래와 같이 배포 코드를 작성해주고 truffle migration 명령어를 통해 배포하였다.
// truffle migration/3_deploy_JwToken.js 파일
const JwToken = artifacts.require('JwToken');
module.exports = function (deployer) {
deployer.deploy(JwToken, 'JwToken', 'JTK', 5000);
};
JwToken 컨트랙트의 CA 를 이용해 메타마스크에서 다음과 같이 토큰 가져오기를 진행할 수 있다.
배포를 진행할 시 인자값으로 전달한 5000 개의 토큰이 컨트랙트 배포자(토큰 발행자)의 계정으로 지급되어 있는 것을 확인할 수 있다. 현재 컨트랙트 배포 계정은 아래와 같이 Account 2 이다.
이제 다른 계정(Account 3)에서 JwToken 컨트랙트의 CA로 이더를 전송했을 때 JwToken 컨트랙트에서 설정한 비율대로 스왑이 진행되는지, CA에는 전달 받은 이더가 보관되어 있는지 확인해보자.
다음과 같이 토큰 발행자인 Account 2의 JTK 토큰 개수는 1000개가 줄고 Account 3의 JTK 토큰 개수는 1000개가 늘어난 것을 확인할 수 있다. ethCanBuy 변수에 할당된 200 만큼 1 ETH 당 JTK 토큰 200개로 스왑 비율이 형성되어 있기 때문에 Account 3가 5 ETH에 해당하는 개수만큼 토큰 스왑이 이루어진 것이다.
그리고 truffle console에서 JwToken CA의 잔액을 조회해보면 다음과 같이 Account 3가 전송한 5 ETH 만큼의 잔액이 조회되는 것을 확인할 수 있다. ( 여기서는 wei 단위로 표현되어 있다. )
마지막으로 JwToken 컨트랙트 배포 계정인 Account 2 에서 CA를 향해 이더를 전송했을 때 전체 토큰 발행량이 증가되는지 확인해보도록 하자.
그리고 JwToken CA의 이더 잔액 역시 10 ETH 만큼 증가한 것을 확인할 수 있다.
'Ethereum' 카테고리의 다른 글
Ethereum/이더리움 - NFT / Remix로 컨트랙트 배포하기 / OpenSea에 NFT 올리기 (2) | 2022.07.26 |
---|---|
Ethereum/이더리움 - OpenZeppelin / 토큰 컨트랙트 / 스왑 컨트랙트 (2) | 2022.07.23 |
Ethereum/이더리움 - Solidity(솔리디티) function payable (0) | 2022.07.21 |
Ethereum/이더리움 - 스마트 컨트랙트로 투표 Dapp 만들기 (0) | 2022.07.18 |
Ethereum/이더리움 - 스마트 컨트랙트로 토큰 발행하기 (0) | 2022.07.17 |