[졸업프로젝트] 직접 발행한 토큰으로 dApp 개발
우리 프로젝트는 물품(중고책) 기증을 받아, 판매 수익이 발생하면 해당 수익금을 기부하는 프로젝트이다.
그 과정에서 기부 절차 및 거래 과정의 투명성을 위해 블록체인 위에서 거래가 진행될 수 있도록 하고, 기부한 지분을 나타내는 "토큰" 시스템을 사용하고자 한다.
자, 그렇다면 물품 구매자가 지불한 금액 만큼, 물품 기증자에게 토큰이 전달될 수 있도록 해야할 것이다.
dApp 개발에는 크게 3가지 과정이 있다.
1. 블록체인 위에서 거래가 이뤄질 수 있게 하는 "스마트 컨트랙트" 작성
-> 가장 포괄적이고 많은 체인에서 호환되는 Solidity 언어로 작성.
2. 이 스마트 컨트랙트가 분산화된 체인 위에서 돌아갈 수 있도록 배포
-> 개인적으로 좋아하는 체인인 Polygon의 테스트넷인 Mumbai Network를 사용.
3. 사용자가 Web2(프론트엔드) 환경에서 블록체인 스마트 컨트랙트와 상호작용할 수 있도록 Web3.js 기반 개발
-> 암호화폐 지갑으로는 가장 리소스가 많고, 대중적인 메타마스크를 사용
그렇다면, 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은, 이더리움 기반 Smart Contract를 사용한 dApp 개발 시 유용한 도구 중 하나로 스마트 컨트랙트와 DApp을 개발, 컴파일, 디버깅, 배포하기위한 완전한 개발환경을 제공한다. Hardhat외에도 Truffle, Ganache 등등이 있었으나 내 경우에는 Openzeppelin에서 제공하는 ERC-20 표준을 대부분 사용하기도 하고, Remix 등의 IDE를 통해 스마트 컨트랙트 코드를 이미 확인한 상태였기 때문에 그냥 Hardhat을 통해 배포 과정만 진행했다.
Hardhat 관련 자료는 해당 링크를 참고해도 좋을 것 같다.
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폴더에서 실행해야 한다.

다음은 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 관련해서 실습 하다가 참고할 내용이 공식 문서 밖에 없고 버전 관리도 너무 안 되서 머리 빠지는 줄 알았기 때문에...
아주 아주 아주 간단하고... 사실 내가 부족해서 눈치 못 챈 허점이 존재하는 스마트 컨트랙트라고 생각하지만.... 원래 첫술에 배부를 수는 없는 법이다!
그럼 졸업 프로젝트 마무리를 향해서....!!! 화이팅 화이팅