일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 세션으로 로그인 구현
- express실행
- cookie-parser 만들어보기
- JWT 하드코딩
- 블록 만들기
- useContext
- 라우트 매개변수
- express.static
- JWT 로그인 기능 구현
- express session
- 라우터 분리
- express router
- nodejs파일업로드
- JWT 만들어보기
- useEffect clean up
- 아이디 중복체크기능
- Uncaught Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider>
- javascript기초
- next 매개변수
- ws 라이브러리
- css기초
- node.js path
- mysql wsl
- FormData()
- 비동기파일업로드
- 시퀄라이즈 기본설정
- 라우터미들웨어 분리
- OAuth 카카오
- 라우터와 미들웨어
- buffer.from
- Today
- Total
즐코
ether-token 스왑 / 토큰발행 컨트랙트+스왑전용 컨트랙트 본문
이번엔 ERC20을 직접 작성하지 않고, 이더리움 재단에서 관리하는 오픈소스인 openzeppelin를 설치하여 그 모듈에서 제공해주는 ERC20 컨트랙트를 상속받아 나만의 토큰을 작성해본다. 또한 발행한 토큰과 이더의 swap에 대한 컨트랙트도 따로 작성하여 토큰-이더를 서로 주고 받았는데 이에 대한 내용을 중점적으로 포스팅해본다.
1. truffle 프로젝트 상에서 openzeppelin-solidity 라이브러리 설치
node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol 이 우리가 상속받으려는 ERC20 컨트랙트 이다.
$ truffle init
$ npm init -y
$ npm i openzeppelin-solidity
2. 나만의 토큰용 컨트랙트 작성 : IceToken.sol
오픈제플린에서 가져온 ERC20을 보면 constructor 생성자함수의 인자로 name과 symbol을 받는다. 이를 그대로 가져온 것이다.
(클래스에서 상속하는 부모 클래스의 생성자 함수를 호출해올때 super()를 썼던 것과 비슷한 개념이라고 보면 될 듯하다)
토큰 발행 시 총 발행량인 5,000 토큰을 컨트랙트 배포자 (토큰 발행자)의 계좌에 보관한다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
contract IceToken is ERC20 {
string public _name = "IceToken";
string public _symbol = "ICE";
uint public _totalSupply = 5000 * (10 ** decimals());
constructor() ERC20(_name, _symbol){
_mint(msg.sender, _totalSupply);
}
}
4. 이더 - 토큰 스왑을 위한 컨트랙트 작성 : EthSwap.sol
먼저 만든 IceToken.sol은 토큰 발행을 위한 컨트랙트였다. 현재 최종 목적인 토큰과 이더와의 스왑을 위해 또 다른 컨트랙트를 작성해준다. 두 컨트랙트의 목적은 다르지만 둘은 결국 서로 상호작용해야한다. 따라서 해당 스왑을 위한 컨트랙트 EthSwap.sol은 발행된 토큰인 IceToken의 내용에 접근할 수 있어야하므로 배포 시에 인자로 발행된 토큰의 CA 값을 넣어줘야함을 인지해야한다. 우선 토큰과 이더를 교환하는 함수는 제외하고, 내부적으로 스왑 시 필요한 데이터를 가져오는 함수들을 작성하였다. 대부분 IceToken 컨트랙트 상의 함수를 이용하여 계정이나 토큰 잔량 등을 리턴하는 함수들이다. 각 함수들이 리턴하는 내용들은 아래 코드에 주석으로 기록해두었다.
여기서 등장하는 token 상태변수의 타입인 ERC20 타입은 import 해온 ERC20 컨트랙트 자체를 의미한다.
또한, this는 ethSwap 컨트랙트 자신을 가리키는 것이며, msg.sender는 함수의 호출자를 의미한다는 걸 생각하면 어려운 함수들은 없다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "../node_modules/openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
contract EthSwap {
ERC20 public token;
uint public rate = 100; // 1 ETH = 100 ICE
// 발행한 ERC20 토큰의 CA를 인자값으로 받아 토큰 컨트랙트에 접근한다.
constructor(ERC20 _token){
token = _token;
}
// 발행한 토큰의 주소 (CA) 가져오기
function getTokenAddress() public view returns (address){
return address(token); // token은 ERC20 데이터타입이므로 address로 형변환
}
// 호출자의 토큰잔량 가져오기
function getTokenBalance() public view returns (uint) {
return token.balanceOf(msg.sender);
}
// 해당 EthSwap 컨트랙트 주소 (CA) 가져오기
// this = 해당 컨트랙트 EthSwap
function getThisAddress() public view returns (address){
return address(this);
}
// EthSwap 실행시킨 계정 가져오기
function getMsgSender() public view returns (address){
return msg.sender;
}
// 토큰 발행자 계정 가져오기
function getTokenOwner() public view returns (address){
return token._owner();
// 오픈제플린 제공 ERC20 에는 존재하지 않아서 따로 추가해주었다.(하기 스크린샷 참고)
}
}
** getTokenOwner()
제플린에서 제공해주는 ERC20에는 owner(토큰 발행자)에 대한 함수가 없다. 따라서, 아래와 같이 코드를 추가적으로 작성해줬다.
토큰-이더 스왑 관련 함수
# ETH => token
우선 미리 정해둔 토큰-이더 교환 비율 (상태 변수 rate) 에 맞게 바꿔줄 토큰양을 미리 계산해주고, 해당 EthSwap CA가 보유한 토큰이 요청계정이 요구하는 토큰양보다 크거나 같은지 확인해줘야한다. 조건이 성립한다면 token.transfer()가 실행된다.
이때, 중요한 포인트는 token.transfer() 함수 실행 시 from의 주체가 어떤 계정인지 캐치하는 것이다.
토큰 배포용 컨트랙트인 IceToken.sol을 다시 보면, 모든 토큰 발행량은 해당 토큰 발행자의 계정(EOA) 으로 들어가게 되어있다. 그런데 현재 작성된 코드를 보면 어떤 특정 계정이 토큰을 사고 싶어해서 EthSwap.buyToken()을 실행시킨다면, 내부의 token.transfer() 함수가 실행될텐데, 이때 이 transfer 함수를 실행시키는 주체 즉, from 속성은 IceToken.sol 컨트랙트 입장에선 토큰 발행자 계정이 아닌 EthSwap인 것이다. 따라서, EthSwap 컨트랙트의 계정(CA)에 스왑을 위한 토큰을 어느정도 보관해두어야 한다.
// ETH로 Token을 사는 경우 1 ETH : 100 Token
function buyToken() public payable{
uint256 tokenAmount = msg.value * rate; // 1 ETH 당 100 token
require(token.balanceOf(address(this)) >= tokenAmount, 'lack of token for eth swap');
// 해당 EthSwap의 CA가 보유한 토큰이 요청받은 토큰양보다 많은가?
token.transfer(msg.sender, tokenAmount);
}
# token => ETH
1/ 우선 요청한 계정의 토큰양이 현재 바꾸려고 요청한 토큰양보다 많은지 체크
2/ 토큰-이더 교환 비율에 맞게 토큰을 이더로 환산한다.
3/ ethSwap의 CA에 바꿔줄 만큼의 이더가 있는지 체크
4/ 이때 토큰을 ethSwap CA계좌로 보내기 위해서, token.transfer(address(this), _amount).send({ from: msg.sender }) 할 경우, from 속성값의 msg.sender에 스왑을 요청한 계정 EOA가 들어가지 않고, EthSwap의 CA가 들어갈 것이다! 위의 buyToken() 코드에서처럼 token.transfer()를 실행시키는 주체는 EthSwap이기 때문이다. 즉, token.transfer 실행 시 스왑 요청 계정(EOA)의 토큰이 아닌 EthSwap, 자기자신의 토큰이 자기 자신의 계정으로 다시 들어가는 것이기 때문에 말이 되지 않는다..
따라서, token.transferFrom([위임주는 계정],[위임받는 계정], [위임해주는 토큰양])을 통해 스왑을 요청하는 계정의 토큰을 EthSwap의 CA에게 보내줘야한다. 자세히 말하자면, 스왑을 요청하는 계정이 교환하려는 토큰을 EthSwap 계정(CA)에게 위임(approve)했다는 전제하에, token.tranferFrom(msg.sender, address(this), etherAmount) 로 자기자신 EthSwap 계정(CA)으로 토큰을 보내는 것이다. (* 해당 코드에선 스왑 요청 계정이 EthSwap 계정에게 교환하려는 토큰양을 위임해주는 approve() 코드는 구현되어있지않다, 따라서 테스트 코드 작성 시에 위임 해주는 부분을 작성하고 해당 함수를 테스트할 것이다)
스왑 요청 계정이 EthSwap CA에게 스왑할 토큰을 위임해주고
=> 이 위임받은 EthSwap계정이 자기자신(EthSwap CA)한테 토큰을 보내는 것
5/ 이렇게 위임받은 토큰을 자기 자신에게 고대로 입금하고, 스왑 요청한 계정에겐 payable(msg.sender).transfer(etherAmount) 로 환산한 이더를 송금해준다.
// Token을 팔아 ETH를 받는 경우
// 제 3자 거래가 필요하다.
function sellToken(uint256 _amount) public payable {
require(token.balanceOf(msg.sender) >= _amount);
// 해당 요청을 실행시킨 계정의 토큰 잔량이 현재 바꾸려고 요청한 토큰양보다 많은가?
uint etherAmount = _amount/rate; // 1 token 당 0.01 ETH
// ex) 50토큰 바꾸면 0.5이더 받아야함
require(address(this).balance >= etherAmount);
// 해당 EthSwap의 CA가 보유한 이더 잔액이 바꿔줘야할 이더 금액보다 큰가?
token.transferFrom(msg.sender, address(this), etherAmount);
// 특정 계정으로부터 위임받은 이더스왑이 그 특정 계정에게 받은 토큰을 자기 자신한테 보낸다.
payable(msg.sender).transfer(etherAmount);
// 이더스왑이 요청한 특정 계정에게 이더를 보내준다.
}
5. 두 컨트랙트 배포하기
위에서 언급한대로 EthSwap 컨트랙트는 IceToken에 접근할 수 있어야 하므로 IceToken의 CA 값을 인자로 받아 배포를 진행해야 한다.
// migration/2_deploy_IceToken.js
const IceToken = artifacts.require("IceToken");
const EthSwap = artifacts.require("EthSwap");
module.exports = async function (deployer) {
try {
await deployer.deploy(IceToken); // IceToken 배포 진행 (tx 진행)
const token = await IceToken.deployed(); // 배포된 인스턴스 가져오기 (tx 가져오기)
await deployer.deploy(EthSwap, token.address); // 토큰 CA 넣어서 발행된 토큰 내용에 접근하게끔
const ethSwap = await EthSwap.deployed(); // EthSwap 배포
} catch (err) {
console.log(err.message);
}
};
6. 테스트 코드 작성해보기
이번엔 describe로 바로 작성하지 않고 contract 메소드 내에서 작성한다. (테스트 코드 실행 시 재배포 진행)
contract의 콜백 함수 상의 인자로 배열을 받아보면 현재 연결되어 있는 이더리움 네트워크 노드의 계정들을 가져와 준다.
항상 첫번째 계정이 배포자가 되므로 첫번째 계정을 deployer라고 지정해주었다.
이번 포스팅은 토큰과 이더의 스왑이 주 목적이다. 따라서 두 함수의 테스트 시 아래 두 가지를 놓치지 않는게 중요하다.
1/ buyToken() 함수 테스트 전 EthSwap CA (스왑용 계정)에 미리 토큰 지급해두기 (transfer)
2/ sellToken() 함수 테스트 시 EthSwap CA (스왑용 계정)에 스왑할 토큰양 위임하기 (approve)
// test/swap.test.js
const IceToken = artifacts.require("IceToken");
const EthSwap = artifacts.require("EthSwap");
function toWei(n) {
return web3.utils.toWei(n, "ether");
}
contract("eth_swap", ([deployer, acct1, acct2]) => {
describe("EthSwap deploy", async () => {
console.log(deployer, acct1, acct2);
//0x00463470726C4a0b674A667114DFd99231b185a4 0xE08510B57200dEDb28933F9A0110D6a2a61aDE4A 0x3C60Bad13D4aDDF4691DcF6f3406c602D2357b10
let token, ethSwap; // 각 컨트랙트 인스턴스를 가져오기 위해서
it("deployedContract 가져오기", async () => {
token = await IceToken.deployed();
ethSwap = await EthSwap.deployed(token.address);
console.log("IceToken CA값: ", token.address);
console.log("EthSwap CA값: ", ethSwap.address);
});
it("토큰 발행자의 초기 토큰 보유량", async () => {
const balance = await token.balanceOf(deployer);
console.log("deployer 초기 토큰 밸런스", balance.toString() / 10 ** 18);
});
it("ethswap-getTokenBalance", async () => {
const ethSwapTokenBal = await ethSwap.getTokenBalance();
console.log(
"getTokenBalance() 실행",
ethSwapTokenBal.toString() / 10 ** 18
);
});
it("ethSwap-getTokenAddress()", async () => {
const tokenCA = await ethSwap.getTokenAddress();
assert.equal(tokenCA, token.address);
});
it("ethSwap-getThisAddress()", async () => {
const ethSwapCA = await ethSwap.getThisAddress();
assert.equal(ethSwapCA, ethSwap.address);
});
it("ethSwap-getMsgSender()", async () => {
const msgSender = await ethSwap.getMsgSender();
assert.equal(msgSender, deployer);
});
it("ethSwap-getTokenOwner()", async () => {
const tokenOwner1 = await token._owner();
const tokenOwner2 = await ethSwap.getTokenOwner();
assert.equal(tokenOwner1, deployer);
assert.equal(tokenOwner2, deployer);
console.log("token 발행자 EOA: ", tokenOwner1, tokenOwner2);
});
it("ethSwap-buyToken()", async () => {
// buyToken() 실행 전에 미리 EthSwap CA에 토큰 지급해두기
await token.transfer(ethSwap.address, toWei("4000")); // 4,000 토큰 지급
const ethSwapTk = await token.balanceOf(ethSwap.address);
const deployerTk = await token.balanceOf(deployer);
console.log("ethSwap 토큰 밸런스 : ", ethSwapTk.toString() / 10 ** 18);
console.log("deployer 토큰 밸런스 : ", deployerTk.toString() / 10 ** 18);
let tokenBalance = await token.balanceOf(acct1);
let ethBalance = await web3.eth.getBalance(acct1);
console.log("acct1 토큰 밸런스 : ", tokenBalance.toString() / 10 ** 18);
console.log("acct1 이더 밸런스 : ", ethBalance.toString() / 10 ** 18);
await ethSwap.buyToken({
from: acct1,
value: toWei("10"),
});
tokenBalance = await token.balanceOf(acct1);
ethBalance = await web3.eth.getBalance(acct1);
console.log(
"buyToken() 실행 후 acct1 토큰 밸런스 : ",
tokenBalance.toString() / 10 ** 18
);
console.log(
"buyToken() 실행 후 acct1 이더 밸런스 : ",
ethBalance.toString() / 10 ** 18
);
const ethSwapToken = await token.balanceOf(ethSwap.address);
const ethSwapEthBal = await web3.eth.getBalance(ethSwap.address);
console.log(
"buyToken() 실행 후 ethSwap CA에 남아있는 토큰 밸런스 : ",
ethSwapToken.toString() / 10 ** 18
);
console.log(
"buyToken() 실행 후 ethSwap CA에 들어온 이더 밸런스 : ",
ethSwapEthBal.toString() / 10 ** 18
);
});
it("ethSwap-sellToken()", async () => {
let swapEth = await web3.eth.getBalance(ethSwap.address);
let swapToken = await token.balanceOf(ethSwap.address);
let acct1Eth = await web3.eth.getBalance(acct1);
let acct1Token = await token.balanceOf(acct1);
console.log(`
EthSwap 이더 밸런스 : ${swapEth / 10 ** 18}
EthSwap 토큰 밸런스 : ${swapToken / 10 ** 18}
acct1 이더 밸런스 : ${acct1Eth / 10 ** 18}
acct1 토큰 밸런스 : ${acct1Token / 10 ** 18}
`);
// acct1이 토큰 교환을 위해 교환할 토큰을 ethSwap에게 위임
await token.approve(ethSwap.address, toWei("1000"), {
from: acct1,
});
// acct1이 위임한 토큰 100개를 ethSwap 자기자신한테 전송
await ethSwap.sellToken(toWei("1000"), {
from: acct1,
});
swapEth = await web3.eth.getBalance(ethSwap.address);
swapToken = await token.balanceOf(ethSwap.address);
acct1Eth = await web3.eth.getBalance(acct1);
acct1Token = await token.balanceOf(acct1);
console.log(`
sellToken() 실행 후 EthSwap 이더 밸런스 : ${swapEth / 10 ** 18}
sellToken() 실행 후 EthSwap 토큰 밸런스 : ${swapToken / 10 ** 18}
sellToken() 실행 후 acct1 이더 밸런스 : ${acct1Eth / 10 ** 18}
sellToken() 실행 후 acct1 토큰 밸런스 : ${acct1Token / 10 ** 18}
`);
});
});
});
테스트 실행 결과는 아래와 같다.
'BlockChain' 카테고리의 다른 글
ERC-721 살펴보기 (0) | 2022.07.28 |
---|---|
ERC-721 / NFT / Remix로 컨트랙트 배포 및 실행 테스트 (0) | 2022.07.27 |
ERC-20 나만의 토큰 작성 / fallback, receive? / 이더로 토큰 사기 (0) | 2022.07.22 |
ERC20 interface / ERC20 (0) | 2022.07.22 |
ETHER 전송을 위한 스마트 컨트랙트 작성 (payable) (0) | 2022.07.21 |