먼저 truffle-config.js 파일을 열어서 다음 부분을 uncomment하고 수정한다.
networks:{development:{host:"127.0.0.1",//Localhost (default: none)port:8545,//StandardEthereumport (default: none)network_id:"*",//Anynetwork (default: none) },...//Configureyourcompilerscompilers:{solc:{version:"0.8.0",//Fetchexactversionfromsolc-bin (default: truffle's version) settings: { // See the solidity docs for advice about optimization and evmVersion optimizer: { enabled: false, runs: 200 }, } }, },
Step #2 토큰 컨트랙트 생성
은행에서 이자로 배포할 토큰은 ERC20 규약을 따르도록 할 것이다. Openzeppelin 라이브러리를 이용해서 이를 처리하도록 한다. 이에 대한 문서는 아래 링크에 있다.
먼저 openzeppelin library를 설치한다. 해당 라이브러리에는 ERC20토큰에 대한 인터페이스가 정의되어 있다. 이를 import하여 상속받아 사용할 것이다.
npminstall@openzeppelin/contracts
새로 생성할 토큰에 대한 solidity 파일을 contract/ 밑에 생성하고(contract/Token.sol) 아래 내용을 입력한다.
//SPDX-License-Identifier:MITpragmasolidity^0.8.0;import"@openzeppelin/contracts/token/ERC20/ERC20.sol";contractTokenisERC20{addresspublicminter; //화폐주조자라는뜻이다.//EventeventMinterChanged(addressindexedfrom,addressto);/***@dev토큰의이름과심볼을ERC20constructor를이용해등록하고,minter를저장한다.**토큰의이름,심볼,minter모두변경할수없다.constructor는단한번만실행된다.*/constructor() payable ERC20("Decentralized Bank Token","DEBT") {minter=msg.sender; //deployer가처음에는minter로등록된다. }/***@dev등록된minter를{newMinter}로바꾸는함수이다.*토큰이생성된후은행을새로운minter로바꾼다.*/functionpassMinterRole(addressnewMinter) public returns (bool) {//deployer만이함수를실행할수있다.require(msg.sender==minter,"Error, only owner can change pass minter role" );minter=newMinter;//Event를발생시킨다.emitMinterChanged(msg.sender,newMinter);returntrue; }/***@devToken을발행하는함수*/functionmint(addressaccount,uint256amount) public {//minter만이함수를실행할수있다.require(msg.sender==minter,"Error, msg.sender does not have minter role" ); //ERC20에정의되어있다._mint(account,amount); }}
Solidity 개발 관습에 맞게 주석을 달아 두었다. 토큰을 위한 코드는 생각보다 간단하다. 이제 truffle로 해당 코드를 컴파일해보자. truffle-config.js 에 따로 설정하지 않았다면 컴파일 결과물은 build/contracts 에 생성된다.
trufflecompile
Step #3 Test our token contract
Token 컨트랙트가 컴파일 되었으니, 문제 없이 동작하는 지 테스트해보도록 한다. 컨트랙트는 한번 배포되면 수정할 수 없으니 다른 프로그래밍에 비해 test과정이 특히 중요하다.
Truffle의 test과정에서 Ganache를 이용하는 것이 좋다. Ganache는 이더리움 로컬 네트워크를 생성하고 100ETH를 가진 10개의 account를 제공해준다.
Test를 진행할 때 사용할 수 있는 여러 assertion module이 있는 데, 이 중 CryptoZombies 튜토리얼에서 쓰였던 chai를 이용할 것 이다. chai는 아래와 같이 다운로드할 수 있다.
npminstallchainpmichai-as-promised
이제 test/ 폴더에 test.js 파일과 helpers/time.js helpers/utils.js 를 생성한다.
Truffle로 테스트를 수행한다. 자동으로 test/ 에 있는 코드를 실행하여 테스트한다.
truffletest
Step #4 Token migration
컴파일된 토큰 컨트랙트를 Ganache가 만들어준 로컬 네트워크에 deploy하자. 이를 migration이라고 한다.
migrations/ 폴더에 1로 시작하는 js파일이 있다. Truffle에서 기본 템플릿으로 제공하는 거라 사실 뭔지 자게하게 모르겠다. migrations/ 폴더의 파일은 숫자 prefixed 파일명을 사용해야 하는데, Truffle이 이 숫자 순서대로 실행시키기 때문이다. Token을 배포하기 위한 2_deploy.js 파일을 생성하고 아래 내용을 붙혀넣는다.
Summary=======> Total deployments: 2> Final cost: 0.0348687 ETH
Decentralized Bank(DeBank) Contract
Step #5 DeBank Contract 작성
이제 예금, 출금, 대출, 상환 등의 기능을 가진 DeBank contract에 대해 코드를 작성할 것이다. 우선 컨트랙트의 기본 뼈대부터 입력한다.
//SPDX-License-Identifier:MITpragmasolidity^0.8.0;import"./Token.sol";contractDeBank{Tokenprivatetoken;//Tokencontract의주소를받아내부private변수에저장한다.constructor(Token_token) {token=_token; }functiondeposit() payable public { }functionwithdraw() public { }functionborrow() payable public { }functionpayOff() public { }}
위 코드에서 payable 제어자(modifier)는 함수가 ETH를 받을 수 있게 한다. 즉, payable 제어자가 붙은 함수를 실행할 때 ETH를 동봉해서 실행할 수 있다.
Deposit함수를 작성하자. 아래 내용을 알맞은 위치에 붙혀넣는다.
contractDeBank{Tokenprivatetoken;//user{address}의예금시점{uint}을저장하는mappingmapping(address =>uint) public depositStart;//user{address}의잔액{uint}을저장하는mappingmapping(address =>uint) public etherBalanceOf;//user{address}가예금을했는지{bool}를저장하는mappingmapping(address =>bool) public isDeposited;//Event는frontend와소통하기위해사용된다.eventDeposit(addressindexeduser,uintetherAmount,uinttimeStart);//Tokencontract의주소를받아내부private변수에저장한다.constructor(Token_token) {token=_token; }/***@devEth를받아예금한다.*/functiondeposit() payable public {//예금중인사람은더예금할수없다. (이자 관리어려움때문에)require(isDeposited[msg.sender]==false,'Error, deposit already active');//예치최소금액을확인한다.msg.value는함수가받은ETH량이저장되어있다.require(msg.value>=1e16,'Error, deposit must be >= 0.01 ETH');etherBalanceOf[msg.sender]=etherBalanceOf[msg.sender]+msg.value;//deposit한시점저장depositStart[msg.sender]=depositStart[msg.sender]+block.timestamp;isDeposited[msg.sender]=true; //Event발생emitDeposit(msg.sender,msg.value,block.timestamp); }
Withdraw 함수도 아래와 같이 작성한다. (역시 주석 참고)
eventWithdraw(addressindexeduser,uintetherAmount,uintdepositTime,uintinterest);functionwithdraw() public {//예금하고있는지확인require(isDeposited[msg.sender]==true,'Error, no previous deposit');uintuserBalance=etherBalanceOf[msg.sender]; //forevent//예금기간확인 (초 단위)uintdepositTime=block.timestamp-depositStart[msg.sender];//이자계산 (10% APY(AnnualPercentageYield) 기준,자세한계산생략)uintinterestPerSecond=31668017* (etherBalanceOf[msg.sender] / 1e16);uintinterest=interestPerSecond*depositTime;//ETH를돌려준다.payable(msg.sender).transfer(etherBalanceOf[msg.sender]); //ethbacktouser//유저에게이자만큼의토큰을발행해준다.token.mint(msg.sender,interest); //interesttouser//초기화depositStart[msg.sender]=0;etherBalanceOf[msg.sender]=0;isDeposited[msg.sender]=false;//이벤트발생emitWithdraw(msg.sender,userBalance,depositTime,interest); }
block.timestamp와 now는 같다. (now 가 block.timestamp의 alias) now는 deprecated이므로 block.timestamp를 사용하자.
Step #6 DeBank Testing
Test 파일은 로직만 잘 작성하면 되므로 자세한 설명은 생략하고 한번 읽어 보길 바란다. 전체 코드는 아래와 같다.
constToken=artifacts.require('./Token')constDecentralizedBank=artifacts.require('./DeBank')constutils=require("./helpers/utils");consttime=require("./helpers/time");require('chai').use(require('chai-as-promised')).should()contract('DeBank', ([deployer, user]) => {lettoken,deBank;constinterestPerSecond=31668017//(10%APY) formin.deposit (0.01 ETH)//beforEachhook은테스트전에매번실행되는함수이다.beforeEach(async () => {token=awaitToken.new()//token.address를넘겨주어DecentralizedBank생성deBank=awaitDecentralizedBank.new(token.address)//token의발행권을deBank로넘겨준다.awaittoken.passMinterRole(deBank.address,{from:deployer}) })//context는testinggroup같은느낌context('testing token contract...', () => {context('success', () => {//it은테스트최소단위이다.it('checking token name',async () => {//아래와같이글로읽히는assertionmodule이chaiexpect(awaittoken.name()).to.be.eq('Decentralized Bank Token') })it('checking token symbol',async () => {expect(awaittoken.symbol()).to.be.eq('DEBT') })it('checking token initial total supply',async () => {expect(Number(awaittoken.totalSupply())).to.eq(0) })it('DeBank should have Token minter role',async () => {//minter를deBank로넘겼으므로minter가deBank여야한다.expect(awaittoken.minter()).to.eq(deBank.address) }) })context('failure', () => {it('passing minter role should be rejected',async () => {//현재minter{deBank}만이passMinterRole을실행할수있다.awaittoken.passMinterRole(user,{from:deployer}).should.be.rejectedWith(utils.EVM_REVERT) })it('tokens minting should be rejected',async () => {//현재minter{deBank}만이mint를실행할수있다. await token.mint(user, '1', {from: deployer}).should.be.rejectedWith(utils.EVM_REVERT) //unauthorized minter
}) }) })context('testing deposit...', () => {letbalancecontext('success', () => {beforeEach(async () => {awaitdeBank.deposit({value: 10**16, from: user}) //0.01 ETH })it('balance should increase',async () => {expect(Number(awaitdeBank.etherBalanceOf(user))).to.eq(10**16) })it('deposit time should > 0',async () => {expect(Number(awaitdeBank.depositStart(user))).to.be.above(0) })it('deposit status should eq true',async () => {expect(awaitdeBank.isDeposited(user)).to.eq(true) }) })context('failure', () => {it('depositing should be rejected',async () => {awaitdeBank.deposit({value: 10**15, from: user}).should.be.rejectedWith(utils.EVM_REVERT) //to small amount }) }) })context('testing withdraw...', () => {letbalancecontext('success', () => {beforeEach(async () => {awaitdeBank.deposit({value: 10**16, from: user}) //0.01 ETHawaitutils.wait(2) //accruinginterestbalance=awaitweb3.eth.getBalance(user)awaitdeBank.withdraw({from: user}) })it('balances should decrease',async () =>{expect(Number(awaitweb3.eth.getBalance(deBank.address))).to.eq(0)expect(Number(awaitdeBank.etherBalanceOf(user))).to.eq(0) })it('user should receive ether back',async () => {expect(Number(awaitweb3.eth.getBalance(user))).to.be.above(Number(balance)) })it('user should receive proper amount of interest',async () => {//timesynchronizationproblemmakeuscheckthe1-3srangefor2sdeposittimebalance=Number(awaittoken.balanceOf(user))expect(balance).to.be.above(0)expect(balance%interestPerSecond).to.eq(0)expect(balance).to.be.below(interestPerSecond*4) })it('depositer data should be reseted',async () => {expect(Number(awaitdeBank.depositStart(user))).to.eq(0)expect(Number(awaitdeBank.etherBalanceOf(user))).to.eq(0)expect(awaitdeBank.isDeposited(user)).to.eq(false) }) })context('failure', () => {it('withdrawing should be rejected',async () =>{awaitdeBank.deposit({value: 10**16, from: user}) //0.01 ETHawaitutils.wait(2) //accruinginterestawaitdeBank.withdraw({from: deployer}).should.be.rejectedWith(utils.EVM_REVERT) //wrong user }) }) })xcontext('testing borrow...', () => { })xcontext('testing payOff...', () => { })})
백엔드로 사용할 블록체인 구현이 완료되었으니 (DeBank의 borrow, payOff는 추후에 추가 구현) 이제 React를 이용해서 프론트엔드 구현한다. 프로젝트 루트 디렉토리에서 아래 커맨드를 입력하여 react 환경을 구성한다.
npxcreate-react-appclient
client 디렉토리로 이동해서 실행해본다.
cdclient&&yarnstart
Step #9 Install dependency & Metamask
필요한 모듈을 설치한다.
npminstallreact-bootstrapbootstrapweb3
브라우저에서 web3 어플리케이션을 사용하기 위해 Metamask 확장 프로그램 설치가 필요하다.
확장 프로그램을 설치하면 브라우저 오른쪽 위에 해당아이콘이 생성된다.
Step #10 Metamask setting
Metamask 계정을 생성한 후 테스트 용도로 쓰기 위해서 Ganache network를 등록해주어야 한다. Etereum mainnet을 클릭하고 Custom RPC를 선택한다.
아래와 같이 입력해서 Ganache Test Network를 설정한다. (포트번호가 7545일수도 있다.)
다음으로 Ganache에 생성되어 있는 10개의 test 계정 중 1개를 import한다. 프로필 사진 같은 것을 누르고 Import Account를 누르고 Ganache에서 열쇠 모양 아이콘을 클릭한 후 private key를 복사해 붙혀넣는다.
Import한 Account를 개발하고 있는 웹 사이트에 conntect 시킨다. 옵션 아이콘을 누르고 connected sites를 누르고 확인을 계속 누르면 된다.
Step #11 React Skeleton code
React 작성에 대한 가이드는 아니니 React 코드에 대한 설명은 생략한다. 아래는 UI를 보여주는 React 템ㅍ플릿 코드이다. 이제 Metamask를 연결해 web3 어플리케이션으로 만들고, deposit과 withdraw함수를 구현해서 이더리움 네트워크(여기서는 Ganache local network)와 상호작용을 해볼것이다.
Deposit했던 ETH가 다시 들어왔다. 그런데 이자로 받은 토큰은 어떻게 확인할까? 토큰을 Metamask에 추가해주어야 지갑에서 확인 가능하다. 토큰 컨트랙트 주소를 복사해서 Metamask의 Assets - Add Token에 붙혀넣는다. Token.json 파일에서 확인 가능하다.
아주 잠깐 예금했기때문에 매우 적지만 이자가 DEBT 토큰으로 계좌에 들어온 것을 확인할 수 있다.