mkdir trufflecd truffletruffle inittruffle-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 -ynpm 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 토큰 규격파일이 있는지 확인합니다.

// 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-solidity의ERC20토큰 규격을 가져와 상속받아 작업하였습니다.- 발행하려는 토큰의 이름과 심볼명, 발행량을 설정해주고
Contract를 최초 배포할때 배포자에게 미리 토큰을 지급하도록 상속받은ERC20.sol파일에 있는_mint함수를 가져와 발행한 모든 토큰을 최초 배포자에게 할당해주었습니다.- 후에
TestCode를 작성할 때 배포한SwanToken의BalanceOf,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);
}
- 실질적으로
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에 대한 스왑을 처리해주어야 하기에 생성자 함수의 인자값으로 배포한SwanToken의CA값을 받아SwanToken과ETH,ETH와SwanToken간의 스왑을 처리해줄 것입니다.buyToken함수가ETH에서Token으로 스왑할 때 사용할 함수입니다.ETH에 대한 거래를 처리할 것이니payable function으로 작성해주었고 위에서 선언한uint public rate = 100을 사용하여1ETH당100SwanToken의 스왑을 처리해줄 것입니다. 중간에require()를 통해ETH를 받고Token을 보내줄CA계정에Token이 보내줄만큼 충분히 있는지 검증하는 코드를 추가하였고 보낼Token양이 충분하다면 해당Contract를 실행한EOA계정에게1ETH당100개의SwanToken을 보내줄 것입니다.sellToken함수가Token에서ETH로 스왑할 때 사용할 함수입니다. 마찬가지로 함수에서ETH를 보내주는 작업을 할 것이니payable function으로 작성해주었고 함수를 실행시킨 계정에 스왑하려는SwanToken을 가지고 있는지require()를 통해 검증해주었고ethSwap의CA계정에서도 보내줘야할ETH양만큼 가지고 있는지 검증을 하였습니다. 작성한 두개의 검증을 모두 통과하였을경우token의transferFrom함수를 사용하여 함수를 실행한EOA계정에서ethSwap의CA계정에게 함수를 실행할 때 인자값으로 받은amount만큼SwanToken을 보내주고ethSwapCA계정에서EOA계정에게ETH를 보내주었습니다.
Ganache테스트 네트워크를 실행하고migration파일을 작성한 후truffle migration으로 배포를 할것인데ethSwap의 생성자 함수 부분에 배포한SwanToken의CA값이 들어가기 때문에SwanToken이 배포가 다 된 후CA값을 가져와서ethSwap Contract를 배포할때 인자값으로 넣어주어야 합니다.migrations디렉토리의 아래와 같이 작성한 파일을 추가해주도록 합시다.
2_deploy_swanToken.jsconst 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로 작성한Contract를 확인하기 전에MetaMask에Ganache에서 제공한 지갑을 2개정도 추가해주고 토큰 가져오기를 통해SwanToken의CA값을 가지고 토큰이 정상적으로 최초 배포자에게 할당되어있는지 확인합니다.

최초 배포자 계정

일반 계정
TestCodeconst 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 Contract의getToken함수가 정상적으로SwanToken의CA값을return하는지 확인합니다.it('EthSwap-getThis Check')에서ethSwap Contract의getThis함수가 정상적으로ethSwap의CA값을return하는지 확인합니다.it('EthSwap-getMsgSender Check')에서ethSwap Contract의getMsgSender함수가 정상적으로 해당Contract를 실행한EOA계정 값을return하는지 확인합니다.it('SwanToken-owner Check')에서SwanToken의owner가 최초 배포자의EOA계정 값과 일치하는지 확인합니다. 이때_owner함수는openzeppelin-solidity에ERC20.sol이 제공해준 것입니다.it('EthSwap-getTokenOwner Check')에서ethSwap의getTokenOwner함수가 정상적으로 최초 배포자의EOA계정값을return하는지 확인합니다.- it('SwanToken-balanceOf Check')에서 `
ethSwap의CA계정에게SwanToken을 보냈을때ethSwapCA계정에 정상적으로SwanToken이 할당되었는지 확인합니다.it('EthSwap-buyToken Check')에서SwanToken을 가지고 있지 않은 일반 계정이ethSwap의buyToken함수로1ETH를Token으로 스왑하였을때 일반계정에1ETH가 정상적으로 차감되고100 SwanToken이 정상적으로 추가되었는지 확인합니다. 또한ethSwap의CA계정에는1ETH가 추가되었는지 확인합니다.it('EthSwap-sellToken Check')에서ethSwap의CA계정과sellToken함수를 실행할account1EOA계정이 각각 가지고있는Token과ETH양을sellToken함수를 실행하기전에 미리 확인해보았습니다. 이제 실제로ethSwapContract가Token을ETH로 스왑해줘야 하기때문에token의approve함수를 실행하여ethSwap의CA계정에게 스왑해줄50만큼의Token을 미리 위임해줍니다. 이제ethSwap의sellToken함수를 실행시켜account1EOA계정에는50만큼SwanToken이 줄어들고ETH는0.5ETH가 늘어날것이고ethSwap의CA계정에는50SwanToken이 늘어나고0.5ETH가 줄어들것입니다.