Cute Bow Tie Hearts Blinking Pink Pointer

블록체인/스마트 컨트랙트

[스마트 컨트랙트, open-zeppelin] 토큰 <-> 이더리움 스왑 구현해보기

청포도 에이드 2022. 7. 22. 16:08
728x90

목차

 

- ERC20

- open zeppelin

- ERC20 변수타입, 메서드, 함수

- 직접 구현해보기

- 테스트 코드(jest)

 

 

 

ERC20

이더리움에는 이더리움의 표준안을 만들기 위해 유저들이 의견을 내는 게시판같은 장소가 있다.

바로 Ethereum Improvement Proposals, EIPs이다.(EIPs 링크)

이 EIPs에는 Core/Networking/Interface/ERC의 네가지 카테고리가 있다.

ERC는 Ethereum Request for Comment의 준말이다.

이 ERC20은 ERC 카테고리의 20번 글이다.(ERC20 링크)

이더리움의 창시자, 비탈릭이 제안한 코인 표준안이다.

인터페이스 규약이 정의되어있는 글이라 보면 된다.

 

OpenZeppelin

이더리움 공식 문서나 각종 커뮤니티에서 많이 소개되는 오픈 소스 프로젝트가 있다.

OpenZeppelin이 그것이다.

ICO를 진행중인 많은 토큰들은 오픈제플린을 기반으로 하고있다고 알고있다.

또, 솔리디티에 익숙해질 수록 쓰기 편해진다.

 

 

따라서 오픈제플린을 사용해 토큰, 이더 스왑을 구현해볼 것이다!

 

ERC20 변수타입, 메서드

 

address

이더리움 계정 주소를 저장하는 변수 타입

부가 설명을 하자면, 이더리움 계정은 Externally Owned Accounts(EOAs)와 Contract accounts의 두가지로 나뉘는데, EOA는 사람이 관리하는 계정이고, Contract accounts는 프로그래밍된 스마트 컨트랙트의 주소 계정이다.

Contract accounts(CA)도 이더를 가질 수 있다.

 

msg.sender

 

기본적으로 가스 비용을 내고 이 컨트랙트 함수를 호출한 유저 계정(EOA)라고 생각하시면 된다.

토큰을 보낼 사람이 가스 비용을 내고 transfer 함수를 호출해야하는 것이다.

 

근데 msg.sender가 EOA가 아닌 컨트랙트 계정이 되는 경우도 있다고 한다.

 

balanceOf

 

balanceOf 함수는 _owner가 보유한 토큰이 몇개인지 알려주는 함수로, public 함수이며 view 함수이다.

view 함수이기에 가스를 소모하지 않는다.

 

msg.value

msg.value는 송금보낸 코인의 값이다.

 

approve, allowance, transferFrom

 함수명 기능 리턴값 
 approve  spender 에게 value 만큼의 토큰을 인출할 권리를 부여한다. 이 함수를 이용할 때는 반드시 Approval 이벤트 함수를 호출해야 한다.  성공 혹은 실패(bool)
 allowance  owner 가 spender 에게 인출을 허락한 토큰의 개수는 몇개인가?  허용된 토큰 개수
 transferFrom  from 의 계좌에서 value 개의 토큰을 to 에게 보내라. 단, 이 함수는 approve 함수를 통해 인출할 권리를 받은 spender 만 실행할 수 있다.  성공 혹은 실패 (bool)

 

require

 

조건의 참과 거짓을 판별하고, 거짓일 경우 예외처리(=실행 중단)하는 키워드입니다.

아예 모든 실행을 취소시키기에 가스 비용은 취소되고 데이터 저장도 복구됩니다.

 

구현해보기

 

 

일단 터미널을 켜고,

 

npx ganache-cli

테스트 메인넷을 실행시켜준다.

 

다른 터미널 열어서,

 

 

mkdir truffle
cd truffle
truffle init
npm init -y
npm install openzeppelin-solidity

 

truffle 기본 세팅 후, 오픈제펠린을 다운로드해준다.

 

truffle/node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol 파일에서 ERC20 규약을 확인할 수 있다.

 

이 파일을 살짝쿵 수정해보겠다...

 

contract 를 시작하면 가장 처음 나오는

 
string private _name;
string private _symbol;

 

아래에

 

address public _owner;

를 추가해주고,

 

construtor 생성자 함수 안에

 

_name = name_;
_symbol = symbol_;

 

아래에도

 

_owner = msg.sender;

를 추가해주자.

 

 

contracts 안에 새 토큰을 만들 파일명을 생성해주자.

 

ethSwap.sol과 gyulToken.sol을 만들 것이다.

 

 

truffle/contracts/gyulToken.sol

 

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.15;

import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
// ERC20 규격 불러오기

contract gyulToken is ERC20{ //ERC20을 상속받아 gyulToken을 만든다.

    string public _name = "gyulToken"; //생성할 토큰명
    string public _symbol = "GTK"; // 토큰 심볼
    uint256 public _totalSupply = 10000 * (10 ** decimals()); //초기 발행량, 공급량

    constructor() ERC20(_name, _symbol){
        _mint(msg.sender, _totalSupply);
    }

}

constructor 안에 _mint 메서드는 스마트 컨트랙트를 배포할 때 사전 정의된 양의 토큰을 발행하기 위해 딱 한 번 실행된다.

 

 

 

 

truffle/contracts/ethSwap.sol

 

ethSwap이라는 contract를 만들어준다.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.15;

import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";

//gyulTkoen
//ethSwap 둘다 배포할것임.
// 서로 연결, 통신하려면? CA 값이 필요하다.

contract ethSwap{
    ERC20 public token;
//  타입  접근자  변수명
    uint public rate = 100; // 1이더당 100토큰으로 교환
    
    // gyulToken CA
    constructor(ERC20 _token){
        token = _token;
        // 배포할 때, ethSwap 안에 _token이라는 인자 값(CA)을 받을 것임.
        // 이 코드에선 gyulToken의 CA

    }
}

migrations 디렉토리 안, deploy.js 파일에서 constructor에 들어갈 파라미터 값(token의 CA값)을 넣어주면 contract가 정상적으로 생성된다.

 

    function getThisAddress() public view returns(address){
        return address(this);
    }

 

address(this) : EthSwap의 CA계정이 나옴.

this 는 이금 이 컨트랙트를 말한다.

 

    function getMsgSender() public view returns(address){
        return msg.sender;
        // 스마트 컨트랙트 작동시키려면 tx필요
        
    }

 

 msg.sender 는 이더스왑을 실행 시킨 사람(배포된 함수를 실행한사람)이다.

 

즉, 내가 gyulToken에서 eth로 바꾸려 요청을 보냈다면 token의 address(CA)가 msg.sender가 되고,

eth에서 gyulToken으로 바꾸려 요청을 보냈다면 ethSwap(현재 컨트랙트)의 CA가 msg.sender가 된다.

 

항상 트랜잭션을 실행시킨 자의 CA가 msg.sender이다.

 

    function getToken() public view returns (address){
        return address(token);
        //gyulToken의 주소. 위에서 constructor에서 받아왔으니까 return 가능.
    }

 

위에서 설명한 것처럼, migrations 파일에서 배포를 진행할 때 토큰의 address 값을 인자값으로 넣어주는데

그 CA값을 리턴시켜주는 함수이다.

 

    function getSwapBalance() public view returns(uint){
        return token.balanceOf(msg.sender);
        //토큰량
    }

 

트랜잭션 요청자의 토큰량 리턴해주는 함수

 

    // 1eth 몇개의 토큰을 줄건가
    // eth -> token  산다
    function buyToken() public payable{
        //1*10**18 * 100 => 1EH
        uint256 tokenAmount = msg.value * rate; // 송금보낼 이더량 * rate(이더 1개당 받을 토큰 개수) 
        require(token.balanceOf(address(this))>=tokenAmount, "error[1]");
        // 보유한 토큰량이 보낼 토큰량보다 많아야 아래 코드 작동
        token.transfer(msg.sender, tokenAmount);
        // 구매자에게 tokenAmount 전송

    }

eth를 token으로 바꾸려면, 트랜잭션 발동자는 ethSwap이다.

(ethSwap이 token으로 바꾸자고 요청했고, 그에 맞춰 token보유자가 기꺼이 교환을 해주는 것이니)

 

token.transfer(msg.sender, tokenAmount); 는

 

다시 말하자면, 요청한 개수의 토큰을 msg.sender(ethSwap.address) 계정으로 전송한다는 것이다.

 

 

    function getTokenOwner() public view returns (address){
        return token._owner();
        // 오너의 토큰 개수
    }

 

가진 토큰 개수를 확인하는 함수

 

    // token -> ETH.sell
    // token - > eth 판다
    function sellToken(uint256 _amount) public payable{
        require(token.balanceOf(msg.sender) >= _amount);
        uint etherAmount = _amount/rate;

        require(address(this).balance >= etherAmount); // 이더 개수가 보내려는 개수보다 많아야함
        token.transferFrom(msg.sender, address(this), _amount);
        //                    from          to          value
        //토큰은 EthSwap.address(CA)에 원하는만큼 주고.
        payable(msg.sender).transfer(etherAmount);
        // 그만큼의 이더 보상을 지급받는다.

    }

이번엔 gyulToken을 가지고 ETH로 교환을 요청한다.

당연히, 교환하고 싶은 토큰의 양보다 가진 토큰의 양이 많아야하며, 이를 require로 검증해준다.

ether도 마찬가지이다. 내가 요청한 이더양보다, ethSwap이 가진 이더양이 더 많아야한다.

 

eth 1개 = 토큰 100개 이기때문에

etherAmount = _amount/ rate(100)

 

검증이 끝났다면, msg.sender(이더로 바꾸자고 요청한 사람의 CA계정, token.address)에서, address(this)-ethSwap.address-로 amount(보낼양, value)만큼 전송한다.

 

 전체코드

 

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.15;

import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";

//gyulTkoen
//ethSwap 둘다 배포할것임.
// 서로 연결, 통신하려면? CA 값이 필요하다.
// EThSwap Tx (gyulToken CA)
    // CA.balanceOf() : call(): 가진 총량 대충 예시를 들자면 이렇겠단 얘기지 실제 코드가 이렇진 않음
    // CA.transfer() : send()
contract ethSwap{
    ERC20 public token;
//  타입    접근    변수명
    uint public rate = 100;
    
    // gyulToken CA
    constructor(ERC20 _token){
        token = _token;

    }

    function getThisAddress() public view returns(address){
        return address(this); // this = EthSwap의 CA계정이 나옴.
    }

    function getMsgSender() public view returns(address){
        return msg.sender; //이더스왑을 실행 시킨 사람(배포된 함수를 실행한사람)
        // 스마트 컨트랙트 작동시키려면 tx필요

        /*
            tx={
                from: msg.sender,
                to ....
            }
         */
        
    }


    function getToken() public view returns (address){
        return address(token);
    }

    function getSwapBalance() public view returns(uint){
        return token.balanceOf(msg.sender);
    }

    // 1eth 몇개의 토큰을 줄건가
    // eth -> token  산다
    function buyToken() public payable{
        //1*10**18 * 100 => 1EH
        uint256 tokenAmount = msg.value * rate;
        require(token.balanceOf(address(this))>=tokenAmount, "error[1]");
        token.transfer(msg.sender, tokenAmount);

    }
    function getTokenOwner() public view returns (address){
        return token._owner();
    }


    // token -> ETH.sell
    // token - > eth 판다
    function sellToken(uint256 _amount) public payable{
        require(token.balanceOf(msg.sender) >= _amount);
        uint etherAmount = _amount/rate;

        require(address(this).balance >= etherAmount);
        token.transferFrom(msg.sender, address(this), _amount); // account1->ethswap 50개받고
        payable(msg.sender).transfer(etherAmount); // ethSwap -> account1 0.5 ether

    }
}

 

 

truffle/migrations/2_deploy_token.js
const gyulToken = artifacts.require("gyulToken");

const EthSwap = artifacts.require("ethSwap");

module.exports = async function (deployer) {
    try {
        await deployer.deploy(gyulToken);
        const token = await gyulToken.deployed(); // Tx 내용 가져오기
        token.address; //CA계정

        await deployer.deploy(EthSwap, token.address); // token CA값
        const ethSwap = await EthSwap.deployed();
    } catch (e) {
        console.log(e.message);
    }
};

gyulToken, EtherSwap을 동시에 가져와서 한 파일에서 배포하는 것이 가능하다.

대신 꼭 try catch문을 사용해주어야 된다고 한다(이유는 나도 모름)

 

await deployer.deploy(EthSwap, token.address);

 

이 부분에 들어간 token.address가 아까,ethSwap.sol 파일에서 맨처음에 작성한 constructor 안 인자 값 _token 이다.

 

코드 다 잘 작성해놓고, 토큰 address값을 안넣어줬다? 그럼 그대로 에러다.

 

생성을 위해선 생성자가 필요한데, 생성 할 때 반드시 address값을 받겠다고 정의해놓고

값을 안 넣어주니 contract가 생성되지 않기에, 당연한 결과이다.

 

테스팅

 

const gyulToken = artifacts.require("gyulToken");
const EthSwap = artifacts.require("ethSwap");

function toEther(n) {
    return web3.utils.toWei(n, "ether");
}

//                        배포자    첫계정  두번째계정
contract("eth swap", ([deployer, account1, account2]) => {
    let token, ethSwap;
    describe("EthSwap deployment", async () => {
        it("describe", async () => {
            token = await gyulToken.deployed();
            ethSwap = await EthSwap.deployed(); // 1000개의 토큰이 존재한다는 뜻.
            console.log(deployer, account1, account2);
            console.log(token.address, ethSwap.address);
            // await token.transfer(ethSwap.address, toEther("1000"));
        });

        it("deployed token check(토큰 배포자의 기본 초기값 확인)", async () => {
            let balance = await token.balanceOf(deployer);
            assert.equal(balance.toString(), "10000000000000000000000");
            // console.log(balance.toString());
        });

        it("ethSwap-getToken()", async () => {
            const address = await ethSwap.getToken();
            console.log(address);
            assert.equal(address, token.address);
        });

        it("ethSwap-getMsgSender, getThisAddress", async () => {
            const msgSender = await ethSwap.getMsgSender();
            const thisAddress = await ethSwap.getThisAddress();
            assert.equal(msgSender, deployer);
            assert.equal(thisAddress, ethSwap.address);
            console.log(msgSender, thisAddress);
        });

        it("token owner 확인", async () => {
            const owner = await token._owner();
            console.log(owner);
            assert.equal(owner, deployer);
        });

        it("ethSwap- getTokenOwner", async () => {
            const owner = await ethSwap.getTokenOwner();
            console.log(owner);
            assert.equal(owner, deployer);
        });

        it("token - balanceOf()", async () => {
            await token.transfer(ethSwap.address, toEther("1000"));
            const balance = await token.balanceOf(ethSwap.address);
            console.log("발란스", balance.toString());
        });

        //erc배포한 사람의 eoc 계정을 알고싶다.

        it("ethSwap - buyToken()", async () => {
            let balance = await token.balanceOf(account1);
            assert.equal(balance.toString(), "0");

            await ethSwap.buyToken({
                from: account1,
                value: toEther("1"),
            });

            const eth = await token.balanceOf(account1);
            console.log(web3.utils.fromWei(balance.toString(), "ether"));

            const ethAccount = await web3.eth.getBalance(account1);
            console.log(ethAccount);

            const ethSwapBalance = await web3.eth.getBalance(ethSwap.address);
            console.log(web3.utils.fromWei(ethSwapBalance));
        });

        it("sellToken", async () => {
            // const account1_balance = await token.balanceOf(account1);
            // console.log(web3.utils.fromWei(account1_balance.toString(), "ether")); //100
            //approve (위임받는 사람, 보낼양)
            //token 주는 행위
            // etherSwap
            //                      위임 받는 사람    돈
            // Account : 100 개(buy토큰에 의해서)
            // 근데 위임을 왜함? account1 실행하는 것 : contract ethSwap transfer
            // ethSwap에서는 위임을 처리한다.

            let swapEth = await web3.eth.getBalance(ethSwap.address);
            let swapToken = await token.balanceOf(ethSwap.address);
            let accountEth = await web3.eth.getBalance(account1);
            let accountToken = await token.balanceOf(account1);
            console.log(`
                swapEth:${swapEth / 10 ** 18}
                swapToken:${swapToken / 10 ** 18}
                accountEth:${accountEth / 10 ** 18}
                accountToken:${accountToken / 10 ** 18}

            `);
            await token.approve(ethSwap.address, toEther("50"), {
                from: account1,
            });

            await ethSwap.sellToken(toEther("50"), {
                from: account1,
            });

            swapEth = await web3.eth.getBalance(ethSwap.address);
            swapToken = await token.balanceOf(ethSwap.address);
            accountEth = await web3.eth.getBalance(account1);
            accountToken = await token.balanceOf(account1);
            console.log(`
                swapEth:${swapEth / 10 ** 18}
                swapToken:${swapToken / 10 ** 18}
                accountEth:${accountEth / 10 ** 18}
                accountToken:${accountToken / 10 ** 18}

            `);
            // 위임없이 가보자
            // account1 토큰을 swapToken에다가 보내길 해보자.

            /*
                {
                    from:account1,
                    to:token.address,
                    data:"0x653safd"
                }
            
            */
            // token from -> to 50개 주고 끝낸다.
            // 1. 토큰ㅇ르 50개를 이더스왑에 보내고
            // 2. 토큰 50개만큼의 이더 보상을 돌려받는것.

            // 제 3자 거래
            // account1 -> ethSwap 50개만큼 사용할 수 있게 처리하고,
            // ethSwap 안에서 코드를 처리하고 transferFrom 이더를 전송할거에요.
        });
    });
});

검증을 위한 테스트 코드이니 참고 바란다.

 

truffle test 하면 실행되고, 그 전에 test라이브러리인 jest도 미리 npm install 해주어야함을 잊지 말자.

 

교환하기 전

 

 

교환 후

 

토큰 50개와 이더 0.5개를 교환한 결과이다.

 

 

Reference

 

https://click-the-button.tistory.com/category/How%20To%20%EC%8B%9C%EB%A6%AC%EC%A6%88/%EC%95%94%ED%98%B8%ED%99%94%ED%8F%90%20%EB%A7%8C%EB%93%A4%EA%B8%B0%20%EA%B8%B0%EC%B4%88

 

'How To 시리즈/암호화폐 만들기 기초' 카테고리의 글 목록

프로그래밍, 웹해킹 공략 블로그

click-the-button.tistory.com

 

https://ko.docs.klaytn.foundation/smart-contract/sample-contracts/erc-20/1-erc20

 

1. ERC-20 스마트 컨트랙트 작성 - Klaytn Docs

스마트 컨트랙트를 배포한 후 추가 토큰을 발행하려면, mint와 같은 새로운 공개 메소드를 도입해야 합니다. 이 메소드는 오직 권한이 있는 사용자만이 발행할 수 있어야 하므로, 주의해서 구현

ko.docs.klaytn.foundation

 

https://dayone.tistory.com/33

 

솔리디티 강좌 32강 - payable,msg.value, 와 이더를 보내는 3가지 함수 (send, transfer, call)

유튜브를 통해, 쉽고 간편하게 이해 해보아요! 구독/좋아요 해주셔서 감사합니다 :) !! https://youtu.be/r0qa0IVUHVI 안녕하세요 오늘은 payable, msg.value 와 코인보내는 3가지 방법 send, transfer, call..

dayone.tistory.com

 

728x90