프로젝트

[졸업프로젝트] 직접 발행한 토큰으로 dApp 개발

공멍 2023. 5. 11. 03:35

우리 프로젝트는 물품(중고책) 기증을 받아, 판매 수익이 발생하면 해당 수익금을 기부하는 프로젝트이다.

 

그 과정에서 기부 절차 및 거래 과정의 투명성을 위해 블록체인 위에서 거래가 진행될 수 있도록 하고, 기부한 지분을 나타내는 "토큰" 시스템을 사용하고자 한다.

 

우리 프로젝트의 간단한 유저 프로세스

 

자, 그렇다면 물품 구매자가 지불한 금액 만큼, 물품 기증자에게 토큰이 전달될 수 있도록 해야할 것이다.

 

당시, 스마트 컨트랙트 작성을 위해 그려봤던 Sequence Diagram.

 

dApp 개발에는 크게 3가지 과정이 있다.

 

1. 블록체인 위에서 거래가 이뤄질 수 있게 하는 "스마트 컨트랙트" 작성

 -> 가장 포괄적이고 많은 체인에서 호환되는 Solidity 언어로 작성.

 

2. 이 스마트 컨트랙트가 분산화된 체인 위에서 돌아갈 수 있도록 배포

-> 개인적으로 좋아하는 체인인 Polygon의 테스트넷인 Mumbai Network를 사용.

 

3. 사용자가 Web2(프론트엔드) 환경에서 블록체인 스마트 컨트랙트와 상호작용할 수 있도록 Web3.js 기반 개발

-> 암호화폐 지갑으로는 가장 리소스가 많고, 대중적인 메타마스크를 사용 

dApp의 구조를 잘 설명해준 이미지 (출처: https://smartbuilds.io/building-a-web3-social-media-dapp-joinspace/)

 

그렇다면, Step By Step으로 해당 과정을 진행해보고자 한다.


0. 개발 환경 구축 - Hardhat, Openzeppelin

 

vscode 안에서 터미널 창을 키고 다음과 같이 명령어를 입력한다.

npm init  // 이후에 설정 ~ 할까요? 하고 물어보는 것들 다 enter 로 ok
npm install -–save-dev hardhat
npx hardhat // Create typescript project 선택. 
// 여기까지 하셨으면 solidity-hardhat 프로젝트가 생성됨
npm install –-save-dev @nomicfoundation/hardhat-toolbox // hardhat의 여러 기능들을 사용하기 위해 설치
npm install @openzeppelin/contracts //Openzeppelin 라이브러리 사용하기 위해 모듈 설치

Hardhat 프로젝트 만들기

Hardhat은, 이더리움 기반 Smart Contract를 사용한 dApp 개발 시 유용한 도구 중 하나로 스마트 컨트랙트와 DApp을 개발, 컴파일, 디버깅, 배포하기위한 완전한 개발환경을 제공한다. Hardhat외에도 Truffle, Ganache 등등이 있었으나 내 경우에는 Openzeppelin에서 제공하는 ERC-20 표준을 대부분 사용하기도 하고, Remix 등의 IDE를 통해 스마트 컨트랙트 코드를 이미 확인한 상태였기 때문에 그냥 Hardhat을 통해 배포 과정만 진행했다.

 

Hardhat 관련 자료는 해당 링크를 참고해도 좋을 것 같다.

https://hardhat.org/

 

Openzeppelin은, dApp의 표준이라고 생각하면 된다. 우리 프로젝트에서 사용하게 될 ERC-20 토큰이 미리 정의되어있다.

해당 라이브러리에 있는 함수들을 상속 받아서 프로젝트에 맞게 적용하면 된다.

 

그 밖에 개발을 위해 필요한 환경들...

 

- 메타마스크 지갑 생성 (Mumbai 네트워크 

- polygon testnet인 Mumbai 네트워크에서 사용할 수 있는 Faucet(개발에 필요한 가스비 지불을 위한 금전적 가치가 없는 토큰)이 필요하다. https://mumbaifaucet.com/ <- 이곳에서 받을 수 있다.

- vscode에서 개발한다면 solidity extension이 필요하다.

 

1. [Solidity] Smart Contract 작성

  • 컨트랙트 작성
    • 새로운 솔리디티 파일을 생성해준다. 이름은 COYToken.sol
    • ERC-20을 상속받는 토큰 컨트랙트를 작성! 
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract COYToken is ERC20, Ownable {
    address private _owner;

    mapping (bytes32 => uint256) public burnedTokens;
    
    event Burned(address indexed account, uint256 amount, bytes32 indexed goal);
    event TxComplete(address indexed buyer, address indexed seller, uint256 amount);
    event Donated(address indexed donater, uint256 amount);

    constructor() ERC20("COYToken", "COY") {
        _owner = msg.sender;

    }

    function donate(address seller) public payable {
        require(msg.value > 0, "No tokens");
        payable(_owner).transfer(msg.value);
        _mint(seller, msg.value);
        emit TxComplete(msg.sender, seller, msg.value);
    }

    function justDonate() public payable {
        require(msg.value > 0, "No tokens deposited");
        payable(_owner).transfer(msg.value);
        _mint(msg.sender, msg.value);
        emit Donated(msg.sender, msg.value);
    }

    function burn(uint256 amount, bytes32 goal) public {
        require(amount > 0, "Amount must be greater than zero");
        require(balanceOf(msg.sender) >= amount, "Not enough balance to burn");
        _burn(msg.sender, amount);
        burnedTokens[goal] += amount;
        emit Burned(msg.sender, amount, goal);
    }
}

크게 세 가지 함수를 확인할 수 있을 것이다.

 

donate: 거래를 통해 기부를 할 수 있는 함수. 돈을 지불하고, 지불한 금액 만큼 기증자(seller)에게 COY 토큰이 민팅 될 것이다.

justDonate: 돈을 지불하면 COY 토큰을 획득할 수 있는 함수. 

burn: bytes32 형식으로 작성된 목표 "캠페인"에 토큰을 소각하여 해당 캠페인에 기부하는 것을 나타내는 함수.

 

또한, 어떤 캠페인에 토큰이 얼마나 기부되었는지 기록하기 위해 키-값의 쌍으로 이뤄진 해시 테이블인 mapping을 통해서 burnTokens을 사용한다.

코드 내에서는 확인할 수 없지만 Openzeppelin 라이브러리의 ERC-20을 상속하면 지갑 주소를 키 값으로 하고, 해당 토큰의 보유량을 값으로 하는 mapping 값, balanceOf 를 또한 갖게 된다. 

 

+) Event란, 블록에 데이터를 저장하면서 EVM Logging 기능을 사용하여 저장된 트랜잭션에 저장된 데이터를 호출할 수 있는 Solidity 문법 개념 중에 하나이다. 해당 dApp Tutorial(Web2 code)에는 사용하지 않지만, 이후 완성된 서비스를 위해서 미리 적어두었다. 

 Event의 데이터는 오프체인에서 JavaScript를 통해 블록의 트랜잭션을 조회하여 확인할 수 있기 때문에, web3.js를 통해 해당 트랜잭션의 값이 제대로 전달되었는지, 트랜잭션 완결이 되었는지 확인할 때 이용될 수 있다.

 

스마트 컨트랙트 코드를 잘 작성했다면, 해당 solidity 파일을 컴파일한다.

 

  • $ npx hardhat compile

컴파일이 성공적으로 이뤄졌다면, artifacts 폴더에 해당 스마트 컨트랙트 관련 내용이 담길 것이다.

2. [Hardhat] Smart Contract 배포

  • deploy 스크립트 작성
    • scripts/deploy.ts -- 이미 해당 파일이 존재한다면 코드 내용을 다음과 같이 바꿔주기만 하면 된다.
import { ethers } from "hardhat";

async function main() {
  const COYToken = await ethers.getContractFactory("COYToken")
  const coyToken = await COYToken.deploy();

  await coyToken.deployed();

  console.log(`COYToken Contract is deployed to ${coyToken.address}`)
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
  • dotenv 설치
    • 터미널에서 다음 명령어를 입력해서 dotenv 모듈을 설치한다. 메타마스크 지갑 private key 처럼 보안에 민감한 환경변수를 다룰 때 유용한 모듈이다. 
    • $ npm install dotenv
  • dotenv 설정 적용 
    • 프로젝트 루트 폴더 아래에 .env 라고 파일을 생성하고 다음과 같이 내용을 입력한다.
    • MUMBAI_URL='https://rpc-mumbai.maticvigil.com'
    • PRIVATE_KEY='<본인private key>'
      • 당연히, MATIC Faucet을 받은 자신의 메타마스크 지갑 private 주소를 넣어야 한다.

메타마스크 지갑에서 Account1 옆에 있는 점 3개 버튼을 누른 뒤,

계정 세부정보 -> 비공개 키 내보내기 를 누르면 Private Key를 알 수 있을 것이다.

하지만, 해당 지갑에 실제 자산이 있다면 Private Key를 통해 누구나 갖고 갈 수 있으니... 관리에 유의해야한다.

  • hardhat.config.ts 파일 수정
    • hardhat.config.ts -- 역시, 이미 해당 파일이 존재한다면 코드 내용을 다음과 같이 바꿔주기만 하면 된다.
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import * as dotenv from "dotenv";

dotenv.config();

const config: HardhatUserConfig = {
  solidity: "0.8.17",
  networks: {
    mumbai: {
      url: process.env.MUMBAI_URL || "",
      accounts:
        process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
    }
  },
  gasReporter: {
    enabled: true,
    currency: "USD",
  },
  etherscan: {
    apiKey: process.env.ETHERSCAN_API_KEY,
  },
  typechain: {
    alwaysGenerateOverloads: true,
  },
};

export default config;
  • deploy contract
    • 터미널창에 컨트랙트를 배포하기 위한 스크립트를 동작시키는 명령어를 입력한다.
    • $ npx hardhat run scripts/deploy.js --network mumbai
    • 잘 배포되었다면 터미널창에 배포된 컨트랙트의 주소가 보여질것이다! 이 주소는 다음 단계에서 사용될 예정이니 기록기록...
    • https://mumbai.polygonscan.com/ 
 

TESTNET Polygon (MATIC) Blockchain Explorer

PolygonScan allows you to explore and search the Polygon blockchain for transactions, addresses, tokens, prices and other activities taking place on Polygon (MATIC)

mumbai.polygonscan.com

  • 이곳 폴리곤 Mumbai 네트워크 스캐너에서 해당 주소를 검색하면 누구나 이 스마트 컨트랙트를 확인할 수 있다.

3. [Web3.js] dApp 개발

-- React 프로젝트를 위한 준비 사항은 생략한다.

간단히 dApp 실습만 확인하고 싶은 사람은 내 깃헙 레포지토리(https://github.com/Yevin-WIN/dAppTutorial) 에 올려두었으니, 이것을 git clone하고 coyd-test 폴더에 들어가서

  • $ npm install

를 실행한 뒤 다음 과정을 진행해도 된다.

 

먼저, src/abi 폴더 만들고 그 밑에 COYToken.json 파일 생성 후, 스마트 컨트랙트 컴파일 시 생겼던 artifacts 폴더안에 있는 COYToken.json 내용을 복붙 해온다.

 

App.js

const COY_ABI = COYToken.abi;
const COY_ADDRESS = "<YOUR CONTRACT ADDRESS>";

여기 COY_Address 옆에 내용에 아까 배포했던 스마트 컨트랙트의 주소를 그대로 붙여넣은 뒤, react 프로젝트를 실행하면 된다.

  • $ npm start
    • 마찬가지로 coyd-test폴더에서 실행해야 한다.

초초초 간단 dApp 완성
버튼을 누르면 스마트 컨트랙트와 상호작용하는 것을 메타마스크에서 확인할 수도 있다.


다음은 app.js 코드가 어떻게 구성되어있는지 간단히 설명하겠다.

 

스마트 컨트랙트를 프론트엔드에서 상호작용할 수 있도록 하는 것은 생각보다 간단한다.

 

$ npm install web3

 

명령어를 통해 web3.js 관련 모듈을 설치하면 끝이다.

 

먼저, 사용자의 메타마스크 지갑과 연결할 수 있는 connectWallet 코드가 필요하다.

async function connectWallet() {
    if (window.ethereum) {
      try {
        const web3 = new Web3(window.ethereum);
        await window.ethereum.enable();
        setWeb3(web3);
        const accounts = await web3.eth.getAccounts();
        setAccounts(accounts);
        const contract = new web3.eth.Contract(COY_ABI, COY_ADDRESS);
        setContract(contract);
        const balance = await contract.methods.balanceOf(accounts[0]).call();
        setBalance(balance);
        const totalSupply = await contract.methods.totalSupply().call();
        console.log(totalSupply);
      } catch (error) {
        console.error(error);
      }
    } else {
      alert("Please install MetaMask to connect to Ethereum network");
    }
  }

 

거래를 통해 기부할 수 있는 스마트컨트랙트 함수였던 "donate"를 호출하여 필요한 값을 전달하고, 해당 값을 pay할 수 있도록 문법에 맞게 작성해주면 누군가에게 암호화폐를 지불하는 것도 어렵지 않다.

  async function donateForTransaction() {
    const gasPrice = await web3.eth.getGasPrice();
    const gasLimit = 300000;
    const totalCost = gasPrice * gasLimit;
    const value = web3.utils.toBN(totalCost).add(web3.utils.toBN(amount));
    await contract.methods.donate('<Foundation Account>').send({ from: accounts[0], value: amount });
    const balance = await contract.methods.balanceOf(accounts[0]).call();
    setBalance(balance);
  }

front end에서 확인할 수 있으려면 간단한 html코드는 필요하겠지?!

<div>
      <button className='button' onClick={connectWallet}>Connect to Wallet</button>
      <div>Account: {accounts.length > 0 ? accounts[0] : "Not connected"}</div>
      <div>COY Token Balance: {balance}</div>
      <div>
        <input type="number" value={amount} onChange={(e) => setAmount(e.target.value)} />
        <button className='button' onClick={donateForTransaction}>거래를 위한 기부</button>
        <button className='button' onClick={justDonate}>그냥 기부</button>
      </div>
      <div>
        <input type="text" value={goal} onChange={(e) => setGoal(e.target.value)} />
        <button className='button' onClick={burnTokens}>Burn Tokens</button>
      </div>
    </div>

 

그냥 이런 과정을 응용하고, 필요한 내용을 검색하고... 반복을 하다보면 원하는 dApp을 만들 수 있을 것이다 ! (ㅎㅎ)

 

Solidity 및 EVM의 좋은 점은... 개발 과정에서 참고할 리소스들이 정말 방대하고 잘 정리되어있다는 점이다.

얼마전에 학회 일로 Solana 랑 Rust 관련해서 실습 하다가 참고할 내용이 공식 문서 밖에 없고 버전 관리도 너무 안 되서 머리 빠지는 줄 알았기 때문에...

 

아주 아주 아주 간단하고... 사실 내가 부족해서 눈치 못 챈 허점이 존재하는 스마트 컨트랙트라고 생각하지만.... 원래 첫술에 배부를 수는 없는 법이다!

 

그럼 졸업 프로젝트 마무리를 향해서....!!! 화이팅 화이팅