블록체인 Block-Chain - 이더리움 ERC-20 토큰 스왑

dev_swan·2022년 7월 21일
0

블록체인

목록 보기
26/36
post-thumbnail

ERC20 토큰 만들기 및 토큰 스왑

truffle 세팅 및 openzeppelin-solidity 설치

  • mkdir truffle
  • cd truffle
  • truffle init
  • truffle-config.js 파일의 development 주석 풀기
development: {
  host: '127.0.0.1', // Localhost (default: none)
    port: 8545, // Standard Ethereum port (default: none)
      network_id: '*', // Any network (default: none)
},
  • npm init -y
  • npm install openzeppelin-solidity
  • truffle 디렉토리를 생성한 후 truffle init 으로 truffle 환경을 구축한 뒤 truffle-config.js 파일을 설정해주고 ERC20, ERC721 등 토큰 규격이 세팅되어 있는 openzeppelin-solidity 프레임워크를 설치하여 ERC20 토큰 생성 및 ERC20 토큰 스왑을 해볼것입니다.
  • openzeppelin-solidity을 설치하였으면 node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol 경로의 ERC20 토큰 규격파일이 있는지 확인합니다.


ERC20 Token Solidity Code 작성

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

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

contract SwanToken is ERC20 {
    string public _name = "swanToken";
    string public _symbol = "STK";
    uint256 public _totalSupply = 10000 * (10 ** decimals());
    
    constructor() ERC20(_name,_symbol) {
        _mint(msg.sender,_totalSupply);
    }
}
  • 배포하려는 Contract SwanToken에 설치한 openzeppelin-solidityERC20 토큰 규격을 가져와 상속받아 작업하였습니다.
  • 발행하려는 토큰의 이름과 심볼명, 발행량을 설정해주고 Contract를 최초 배포할때 배포자에게 미리 토큰을 지급하도록 상속받은 ERC20.sol 파일에 있는 _mint 함수를 가져와 발행한 모든 토큰을 최초 배포자에게 할당해주었습니다.
  • 후에 TestCode를 작성할 때 배포한 SwanTokenBalanceOf, Transfer 등의 여러 함수들이 필요할 것인데 이러한 함수들은 openzeppelin-solidity에서 대부분의 필요한 함수들은 미리 ERC20.sol파일에 정의해두고 있기 때문에 SwanToken Contract에서 작성하지 않아도 됩니다.
  • ERC20.sol _mint 함수
    function _mint(address account, uint256 amount) internal virtual {
        require(account != address(0), "ERC20: mint to the zero address");

        _beforeTokenTransfer(address(0), account, amount);

        _totalSupply += amount;
        _balances[account] += amount;
        emit Transfer(address(0), account, amount);

        _afterTokenTransfer(address(0), account, amount);
    }

ethSwap Solidity Code 작성

  • 실질적으로 ETH를 받고 Token으로 스왑해주거나 Token을 받고 ETH로 스왑해줄 Contract를 작성합니다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

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

contract EthSwap {
    ERC20 public token; // import 해온 ERC20.sol 파일에서 작성한 ERC20 컨트랙트의 public 부분을 모두 interface로 사용 
    uint public rate = 100; // 1 Ether당 Token 비율 1:100

    // ERC20 토큰에 대한 CA 값이 인자값으로 들어갑니다.
    constructor(ERC20 _token) {
        token = _token;
    }

    function getToken()public view returns(address) {
        return address(token); // address(token) = swanToke의 CA 계정을 가져옴
    }
    
    function getThis() public view returns(address) {
        return address(this); // address(this) = ethSwap의 CA계정을 가져옴
    }

    function getMsgSender() public view returns(address) {
        return msg.sender; // 해당 컨트랙트를 실행시킨 EOA계정을 가져옴
    }

    function getTokenOwner() public view returns(address) {
        return token._owner(); // swanToken의 owner ( 최최 배포자 ) EOA계정을 가져옴
    }

    // ETH -> Token 구매
    function buyToken() public payable {
        uint256 tokenAmount = msg.value * rate; // 보내줄 토큰양 구하기

        require(token.balanceOf(address(this)) >= tokenAmount,"Not enough Tokens "); // EthSwap의 CA계정이 Token을 보내줄양이 충분한지 검증

        token.transfer(msg.sender,tokenAmount); // Contract 실행한 EOA계정에게 토큰 보내주기
    }

    // Token -> ETH 구매
    function sellToken(uint256 _amount) public payable {
        require(token.balanceOf(msg.sender) >= _amount); // Contract를 실행시킨 EOA계정의 스왑하려는 swanToken만큼의 양이 있는지 검증
        uint256 etherAmount = _amount/rate; // 받은 SwanToken만큼 줘야할 ETH의 양

        require(address(this).balance >= etherAmount); //ethSwap CA계정이 가지고 있는 eth가 보내줘야하는 eth만큼 있는가 검증
        token.transferFrom(msg.sender, address(this),_amount); // Contract를 실행시킨 EOA 계정이 ethSwap CA계정에게 _amount 만큼 SwanToken을 보냅니다.
        payable(msg.sender).transfer(etherAmount); // ethSwap CA계정에서 Contract를 실행시킨 EOA 계정에게 etherAmount만큼의 ETH를 보내줍니다.
    }
}
  • 해당 Contract는 배포한 SwanToken에 대한 스왑을 처리해주어야 하기에 생성자 함수의 인자값으로 배포한 SwanTokenCA값을 받아 SwanTokenETH, ETHSwanToken간의 스왑을 처리해줄 것입니다.
  • buyToken 함수가 ETH에서 Token으로 스왑할 때 사용할 함수입니다. ETH에 대한 거래를 처리할 것이니 payable function으로 작성해주었고 위에서 선언한 uint public rate = 100을 사용하여 1ETH100SwanToken의 스왑을 처리해줄 것입니다. 중간에 require()를 통해 ETH를 받고 Token을 보내줄 CA계정에 Token이 보내줄만큼 충분히 있는지 검증하는 코드를 추가하였고 보낼 Token양이 충분하다면 해당 Contract를 실행한 EOA계정에게 1ETH100개의 SwanToken을 보내줄 것입니다.
  • sellToken함수가 Token에서 ETH로 스왑할 때 사용할 함수입니다. 마찬가지로 함수에서 ETH를 보내주는 작업을 할 것이니 payable function으로 작성해주었고 함수를 실행시킨 계정에 스왑하려는 SwanToken을 가지고 있는지 require()를 통해 검증해주었고 ethSwapCA계정에서도 보내줘야할 ETH양만큼 가지고 있는지 검증을 하였습니다. 작성한 두개의 검증을 모두 통과하였을경우 tokentransferFrom 함수를 사용하여 함수를 실행한 EOA계정에서 ethSwapCA계정에게 함수를 실행할 때 인자값으로 받은 amount만큼 SwanToken을 보내주고 ethSwap CA계정에서 EOA계정에게 ETH를 보내주었습니다.

Contract 배포하기

  • Ganache 테스트 네트워크를 실행하고 migration 파일을 작성한 후 truffle migration으로 배포를 할것인데 ethSwap의 생성자 함수 부분에 배포한 SwanTokenCA값이 들어가기 때문에 SwanToken이 배포가 다 된 후 CA값을 가져와서 ethSwap Contract를 배포할때 인자값으로 넣어주어야 합니다. migrations 디렉토리의 아래와 같이 작성한 파일을 추가해주도록 합시다.
  • 2_deploy_swanToken.js
const SwanToken = artifacts.require('SwanToken');
const EthSwap = artifacts.require('EthSwap');

// SwanToken을 배포한뒤 EthSwap을 배포
module.exports = async function (deployer) {
	try {
		await deployer.deploy(SwanToken);

		const token = await SwanToken.deployed(); // Transaction 내용 가져오기

		await deployer.deploy(EthSwap, token.address); // EthSwap을 배포하는데 전에 배포한 SwanToken의 CA값을 가져와서 인자값으로 넣어줌

		const ethSwap = await EthSwap.deployed();
	} catch (e) {
		console.error(e);
	}
};

  • truffle migration으로 배포를 해보면 작성한 2_deploy_swanToken.js 파일에서 2개의 Contract가 순차대로 배포된것을 확인할 수 있습니다.

TestCode 작성

  • TestCode로 작성한 Contract를 확인하기 전에 MetaMaskGanache에서 제공한 지갑을 2개정도 추가해주고 토큰 가져오기를 통해 SwanTokenCA값을 가지고 토큰이 정상적으로 최초 배포자에게 할당되어있는지 확인합니다.

최초 배포자 계정

일반 계정


  • TestCode
const SwanToken = artifacts.require('SwanToken');
const EthSwap = artifacts.require('EthSwap');

function toEther(number) {
	return web3.utils.toWei(number, 'ether');
}

// [deployer,account1,account2] = 배포한 사람의 계정 , 1번째 index 계정 , 2번째 index 계정
contract('Eth-Swap', ([deployer, account1, account2]) => {
	let token, ethSwap;

	describe('EthSwap-Deployment', () => {
		console.log(deployer, account1, account2);
		// deployer : 0xe73D6a517a2562c8a289a2E583572C27ee6d315A
		// account1 : 0xD209b18e8910A8D79Fa38c1C118f93D1b0dD4449
		// account2 : 0x605A698126D3233783767cFbc54C378Cd5176FEf

		it('deployed', async () => {
			token = await SwanToken.deployed();
			ethSwap = await EthSwap.deployed();

			console.log('SwanToken CA : ', token.address);
			console.log('EthSwap CA : ', ethSwap.address);
		});

		it('토큰 배포자의 최초 토큰 보유량 확인', async () => {
			const balance = await token.balanceOf(deployer);

			assert.equal(balance.toString(), '10000000000000000000000'); // 두 인자값이 서로 같은지 확인해주는 Test Tool
		});

		it('Ethswap-getToken Check', async () => {
			const address = await ethSwap.getToken.call();

			assert.equal(address, token.address); // ethSwap의 getToken 함수가 SwanToken의 CA값을 return하는지 확인
		});

		it('EthSwap-getThis Check', async () => {
			const thisAddress = await ethSwap.getThis.call();

			assert.equal(thisAddress, ethSwap.address); // ethSwap의 getThis 함수가 ethSwap의 CA값을 return하는지 확인
		});

		it('EthSwap-getMsgSender Check', async () => {
			const msgSender = await ethSwap.getMsgSender.call();

			assert.equal(msgSender, deployer); // ethSwap의 getToken 함수가 컨트랙트를 실행한 EOA값을 retrun하는지 확인
		});

		it('SwanToken-owner Check', async () => {
			const owner = await token._owner();

			assert.equal(owner, deployer); // SwanToken의 owner가 배포한 EOA 계정과 같은지 확인
		});

		it('EthSwap-getTokenOwner Check', async () => {
			const owner = await ethSwap.getTokenOwner();

			assert.equal(owner, deployer); // ethSwap의 getTokenOwner 함수가 SwanToken의 최초 배포자 EOA 계정을 return하는지 확인
		});

		it('SwanToken-balanceOf Check', async () => {
			await token.transfer(ethSwap.address, toEther('1000')); // CA계정의 Token을 전송해줍니다.

			const balance = await token.balanceOf(ethSwap.address);
			console.log(web3.utils.fromWei(balance.toString()), 'swanToken'); // ethSwap의 CA 계정에 Token이 있는지 확인
		});

		it('EthSwap-buyToken Check', async () => {
			let balance = await token.balanceOf(account1); // 1번째 index 계정의 token 보유량 확인

			assert.equal(balance.toString(), '0'); // 토큰을 가지고 있지 않는것을 확인

			await ethSwap.buyToken({
				from: account1,
				value: toEther('1'), // 1 Eth만큼 Token을 구매
			});

			balance = await token.balanceOf(account1); // Ether로 Token을 구매한뒤 다시 1번째 index 계정의 token 보유량 확인

			assert.equal(web3.utils.fromWei(balance.toString(), 'ether'), '100'); // Token을 100개 가지고 있는지 확인

			const eth = await web3.eth.getBalance(account1); // account1의 1 Eth가 차감됬는지 확인
			console.log(web3.utils.fromWei(eth), 'ether');

			const ethSwapBalance = await web3.eth.getBalance(ethSwap.address); // CA계정의 1 Eth가 추가되었는지 확인
			console.log(web3.utils.fromWei(ethSwapBalance), 'ether');
		});

		it('EthSwap-sellToken Check', async () => {
			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);

			// SwanToken을 ETH로 스왑하기 전
			console.log(`swapETH : ${web3.utils.fromWei(swapETH, 'ether')}`, `swapToken : ${web3.utils.fromWei(swapToken, 'ether')}`, `accountETH : ${web3.utils.fromWei(accountETH, 'ether')}`, `accountToken : ${web3.utils.fromWei(accountToken, 'ether')}`);

			// approve ( 위임받는 사람, 보낼양 )
			// ethSwap CA값의 50만큼의 SwanToken을 위임해둠
			await token.approve(ethSwap.address, toEther('50'), { from: account1 });

			// 실제로 SwanToken을 ETH로 스왑함
			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);

			// SwanToken을 ETH로 스왑한 후
			console.log(`swapETH : ${web3.utils.fromWei(swapETH, 'ether')}`, `swapToken : ${web3.utils.fromWei(swapToken, 'ether')}`, `accountETH : ${web3.utils.fromWei(accountETH, 'ether')}`, `accountToken : ${web3.utils.fromWei(accountToken, 'ether')}`);
		});
	});
});
  • it('deployed')에서 Contract들을 다시 배포하고 해당 Contract들의 CA값을 확인합니다.
  • it('토큰 배포자의 최초 토큰 보유량 확인') 에서 최초 배포한 계정이 발행한 모든 SwanToken을 가지고 있는지 확인합니다. assert.equal 메서드는 인자값으로 받은 2개의 값이 서로 다른지 확인해주는 메서드입니다. 서로 다를경우 Error를 보여줍니다.
  • it('Ethswap-getToken Check')에서 ethSwap ContractgetToken함수가 정상적으로 SwanTokenCA값을 return하는지 확인합니다.
  • it('EthSwap-getThis Check')에서 ethSwap ContractgetThis함수가 정상적으로 ethSwapCA값을 return하는지 확인합니다.
  • it('EthSwap-getMsgSender Check')에서 ethSwap ContractgetMsgSender함수가 정상적으로 해당 Contract를 실행한 EOA 계정 값을 return하는지 확인합니다.
  • it('SwanToken-owner Check')에서 SwanTokenowner가 최초 배포자의 EOA계정 값과 일치하는지 확인합니다. 이때 _owner 함수는 openzeppelin-solidityERC20.sol이 제공해준 것입니다.
  • it('EthSwap-getTokenOwner Check')에서 ethSwapgetTokenOwner함수가 정상적으로 최초 배포자의 EOA 계정값을 return하는지 확인합니다.
  • it('SwanToken-balanceOf Check')에서 `ethSwapCA계정에게 SwanToken을 보냈을때 ethSwap CA계정에 정상적으로 SwanToken이 할당되었는지 확인합니다.
  • it('EthSwap-buyToken Check')에서 SwanToken을 가지고 있지 않은 일반 계정이 ethSwapbuyToken 함수로 1ETHToken으로 스왑하였을때 일반계정에 1ETH가 정상적으로 차감되고 100 SwanToken이 정상적으로 추가되었는지 확인합니다. 또한 ethSwapCA계정에는 1ETH가 추가되었는지 확인합니다.
  • it('EthSwap-sellToken Check')에서 ethSwapCA계정과 sellToken함수를 실행할 account1 EOA 계정이 각각 가지고있는 TokenETH양을 sellToken함수를 실행하기전에 미리 확인해보았습니다. 이제 실제로 ethSwap ContractTokenETH로 스왑해줘야 하기때문에 tokenapprove 함수를 실행하여 ethSwapCA계정에게 스왑해줄 50만큼의 Token을 미리 위임해줍니다. 이제 ethSwapsellToken 함수를 실행시켜 account1 EOA계정에는 50만큼 SwanToken이 줄어들고 ETH0.5ETH가 늘어날것이고 ethSwapCA계정에는 50 SwanToken이 늘어나고 0.5ETH가 줄어들것입니다.

0개의 댓글