Blockchain

#27 ERC-20 규격의 토큰을 만들고 이를 스왑하기

Sila 2022. 7. 21. 21:57

 

ERC-20은 이더리움 네트워크 내에서 사용될 토큰의 규격이라고 배운 적이 있는데, 바꿔 말하면

 

이는 이더리움 제작자 측에서는 이 규격에 맞춰 토큰을 만들고 발행하는 것을 어느 정도는

 

권장한다는 것을 의미한다.

 

그래서 이더리움 재단에서 만든 토큰을 쉽게 만들어낼 수 있는 툴이 존재한다.

 

openzeppelin-solidity 라는 라이브러리가 존재하는데, npm으로 설치해주면 된다.

 

npm i openzeppelin-solidity

 

이를 설치하고 node_modules/openzeppelin-solidity/contracts/token/erc20/erc20.sol 의 솔리디티 파일을 열어보면

 

안에 rec20 규격에 맞춘 토큰에 대한 컨트랙트가 존재한다. 이를 그대로 상속해서 써도 좋고, 필요하다면 약간의 수정을

 

가해서 사용하기도 한다.

 

1. token 만들기

token을 만들되, 우린 ERC20.sol 파일을 약간 수정을 가해 최초발행자에 대한 정보를 넣을 것이다.

 

선언 시, address 타입 public 속성으로 _owner를 선언하고, 생성자 함수 실행시 이 값을 msg.sender로 지정해주면 된다.

 

address public _owner;

constructor(string memory _name, string memory symbol_) {
    _owner = msg.sender;
    // ...중략
}

 

/*  truffle/contracts/abcToken.sol  */

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
// ERC20.sol을 import해온다.

contract abcToken is ERC20 {
// abcToken에 ERC20 컨트랙트를 상속시킨다.
    string public _name = "abcToken";
    string public _symbol = "ABC";
    uint256 public _totalSupply = 100000 * (10**decimals());

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

 

2. swap 컨트랙트 만들기

이제 만든 abcToken을 이더리움과 교환해주는 컨트랙트를 작성해보자.

 

/*  truffle/contracts/EthSwap.sol  */

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

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

contract EthSwap {
    ERC20 public token;
    uint256 public rate;

    constructor(ERC20 _token) {
        token = _token;
        rate = 100;
    }

    function getThisAddress() public view returns (address) {
        return address(this);
    }
    // 이 컨트랙트의 ca

    function getMsgSender() public view returns (address) {
        return msg.sender;
    }
    // 이 컨트랙트에 요청을 보낸 address

    function getToken() public view returns (address) {
        return address(token);
    }
    // token contract의 ca를 가져옴

    function getSwapBalance() public view returns (uint256) {
        return token.balanceOf(msg.sender);
    }
    // 요청을 보낸 사람이 요청한 토큰의 잔액을 보여주는 함수 

    function getTokenOwner() public view returns (address) {
        return token._owner();
    }
    // 토큰 최초발행자를 리텅

    function buyToken() public payable {
        uint256 tokenAmount = msg.value * rate;
        require(token.balanceOf(address(this)) >= tokenAmount, "error [1]");
        token.transfer(msg.sender, tokenAmount);
    }
    // 사용자가 보낸 이더리움 갯수의 일정 배율을 곱해 그 만큼의 토큰을 사용자에게 준다.
}

 

그런데 이렇게 하려면 우선 abcToken에 대한 컨트랙트가 먼저 블록체인 네트워크 상에 올라가 있고, 그 후에

 

swap에 대한 컨트랙트가 올라가야 ethSwap 함수들이 사용하는 token이라는 변수의 값이 존재할 수가 있게 된다.

 

(먼저 대상이 될 토큰이 있어야 이더리움과 토큰을 스왑할 기능을 가진 컨트랙트가 동작할 수 있다.)

 

따라서 이 경우센 배포 순서가 중요해지는데, 이를 맞춰 주기 위해서 migration에 관련된 파일을 지금까지와는

 

다른 방법으로 작성해줄 필요가 있다.

 

/*  truffle/migrations/2_deploy_token.js  */

const abcToken = artifacts.require('abcToken');
const EthSwap = artifacts.require('EthSwap');
// 2개의 컨트랙트를 다 가져온다

module.exports = async function (deployer) {
    try {
        await deployer.deploy(abcToken);
        const token = await abcToken.deployed()
        // abcToken을 먼저 배포한 후, 변수에 담는다.
        
        await deployer.deploy(EthSwap, token.address);
        const ethSwap = await EthSwap.deployed()
        // 다음으로 EthSwap을 배포하는데, 이 때 인자값으로
        // 먼저 배포한 abcToken 컨트랙트의 ca를 같이 준다.
    }
    catch (e) {
        console.log(e.message)
    }
}

 

2. 테스트

이제 이 기능들이 잘 동작하는지 테스트 해보면 된다. test 폴더에 swap.test.js 파일을 만들어 다음과 가이 작성한다.

 

const abcToken = artifacts.require('abcToken')
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 () => {
        console.log(deployer, account1, account2)
        // 0, 1, 2번 지갑 주소 (in ganache 네트워크)

        it('deployed', async() => {
            token = await abcToken.deployed()
            ethswap = await EthSwap.deployed()
            console.log(token.address, ethswap.address);
            // abcToken 컨트랙트의 ca, EthSwap 컨트랙트의 ca
            // 테스트 할 때마다 새로 배포되므로 일정한 값이 아니다.
        })

        it(' token 배포자의 초기값', async () => {
            const balance = await token.balanceOf(deployer);
            assert.equal(balance.toString(), '100000000000000000000000')
        })
        // 초기 배포자의 token이 지정한 양만큼 있는지 확인
        // true가 나와야 한다.

        it('ethSwap-Migration', async () => {
            const address = await ethswap.getToken()
            assert.equal(address, token.address)
            // ethswap 함수에서 토큰의 주소를 불러오면
            // abcToken 토큰의 컨트랙트의 ca를 준다.
            // 두 값이 같으므로 true가 나와야 한다.
        })

        it('ethswap-getMsgSender, getThisAddress', async() => {
            const thisaddress = await ethswap.getThisAddress();
            assert.equal(thisaddress, ethswap.address)
            // 둘 모두 etherswap의 address를 반환
        })

        it('token - owner', async() => {
            const owner = await token._owner()
            assert.equal(owner, deployer)
            // 우리가 정한 owner가 곧 배포자이므로 true
        })

        it('etherswap - getTokenOwner', async() => {
            const owner = await ethswap.getTokenOwner()
            assert.equal(owner, deployer)
            // 스왑 컨트랙트도 위와 동일하다.
        })

        it('token - balanceOf()', async() => {
            await token.transfer(ethswap.address, toEther('1000'))
            // 스왑 컨트랙트에게 abcToken 1000개를 준다.
            const balance = await token.balanceOf(ethswap.address)
            console.log(balance.toString())
            // 잔고를 출력한다.
        })

        it('etherswap - buyToken()', async ()=> {
            let balance = await token.balanceOf(account1)
            assert.equal(balance.toString(), '0')
            // abcToken이 0개인지 확인

            await ethswap.buyToken({
                from : account1,
                value : toEther('1')
            })
            // 스왑 컨트랙트에 이더리움을 보내면서 butToken 함수를 실행시킨다.

            balance = await token.balanceOf(account1)
            console.log(web3.utils.fromWei(balance.toString(), 'ether'))
            // 우리가 정한 비율만큼 abcToken 갯수를 맞춰 잘 들어왔는지 확인한다.

            const eth = await web3.eth.getBalance(account1)
            console.log(eth)
            // account1의 이더리움의 잔량을 확인한다.

            const etherSwapBalance = await web3.eth.getBalance(ethswap.address)
            console.log(web3.utils.fromWei(etherSwapBalance))
            // 스왑 컨트랙트가 가진 이더리움의 수를 확인한다.
            // account1이 보낸만큼의 이더리움이 있어야 한다.
        })
    })
})

 

테스트 코드를 돌려 모든 항목에 에러가 없는지 하나씩 확인해보면 된다.