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 토큰 규격파일이 있는지 확인합니다.
// 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
을 보내주고ethSwap
CA
계정에서EOA
계정에게ETH
를 보내주었습니다.
Ganache
테스트 네트워크를 실행하고migration
파일을 작성한 후truffle migration
으로 배포를 할것인데ethSwap
의 생성자 함수 부분에 배포한SwanToken
의CA
값이 들어가기 때문에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
로 작성한Contract
를 확인하기 전에MetaMask
에Ganache
에서 제공한 지갑을 2개정도 추가해주고 토큰 가져오기를 통해SwanToken
의CA
값을 가지고 토큰이 정상적으로 최초 배포자에게 할당되어있는지 확인합니다.
최초 배포자 계정
일반 계정
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 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
을 보냈을때ethSwap
CA
계정에 정상적으로SwanToken
이 할당되었는지 확인합니다.it('EthSwap-buyToken Check')
에서SwanToken
을 가지고 있지 않은 일반 계정이ethSwap
의buyToken
함수로1ETH
를Token
으로 스왑하였을때 일반계정에1ETH
가 정상적으로 차감되고100 SwanToken
이 정상적으로 추가되었는지 확인합니다. 또한ethSwap
의CA
계정에는1ETH
가 추가되었는지 확인합니다.it('EthSwap-sellToken Check')
에서ethSwap
의CA
계정과sellToken
함수를 실행할account1
EOA
계정이 각각 가지고있는Token
과ETH
양을sellToken
함수를 실행하기전에 미리 확인해보았습니다. 이제 실제로ethSwap
Contract
가Token
을ETH
로 스왑해줘야 하기때문에token
의approve
함수를 실행하여ethSwap
의CA
계정에게 스왑해줄50
만큼의Token
을 미리 위임해줍니다. 이제ethSwap
의sellToken
함수를 실행시켜account1
EOA
계정에는50
만큼SwanToken
이 줄어들고ETH
는0.5ETH
가 늘어날것이고ethSwap
의CA
계정에는50
SwanToken
이 늘어나고0.5ETH
가 줄어들것입니다.