이번 포스팅에서는 스마트 컨트랙트를 이용해 간단한 투표 앱을 만들어 보고자 한다.
투표 앱의 전반적인 기능은 다음과 같이 구성해보았다.
- 후보자 초기화 - 스마트 컨트랙트 배포 진행시 후보군 등록
- 후보자에 대한 투표 기능
- 후보자에 대한 득표수 확인
스마트 컨트랙트의 작성 및 배포/실행은 truffle을 이용하였으며 Ganache를 사용해 로컬 이더리움 네트워크에 배포를 진행하였다.
Voting이라는 이름으로 스마트 컨트랙트를 작성하였으며 candidateList 와 votesReceived 라는 두 가지 상태변수를 만들어주었다. candidateList는 string 데이터 타입을 요소로 갖는 배열( string[ ] ) 형태로 만들어주었으며 constructor( ) 함수를 이용해 배포를 진행하는 시점에 후보군으로 등록할 사람들을 candidateList 에 할당하였다. votesReceived 에는 mapping( ) 을 사용했으며 mapping( string => uint8 ) 형태로 만들어 특정 후보자가 몇 표를 얻었는지 votesReceived 를 조회함으로써 확인 가능하게끔 하였다. 또한 candidateList 와 votesReceived 상태변수 모두 접근 제한자를 public으로 하여 getter 함수가 만들어지도록 하였다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
contract Voting {
string[] public candidateList;
mapping(string => uint8) public votesReceived;
/*
{
'후보자1' : 0,
'후보자2' : 2,
}
*/
// 컨트랙트 배포 시점에서 candidateList에 값 할당.
constructor(string[] memory candidateNames) {
candidateList = candidateNames;
}
}
다음으로 투표를 진행할 때 후보군으로 등록된 사람에게만 투표를 진행할 수 있도록 하는 validCandidate( ) 이라는 검증용 함수를 만들어주었다. validCandidate( ) 함수 안에서는 candidateList 배열 안에 있는 모든 후보들을 조회하여 입력한 후보자가 candidateList 안에 있는 후보자인지 여부를 검사한다. 그리고 해당 함수는 스마트 컨트랙트 안에서만 사용되는 함수이기에 접근 제한자를 private으로 하였다.
function validCandidate(string memory candidate) private view returns (bool) {
// 1. 후보자 리스트 : candidateList
// 2. candidateList 안에 입력한 후보자와 일치하는 후보자가 있는지 검증
for (uint i = 0; i < candidateList.length; i++) {
if (keccak256(abi.encodePacked(candidateList[i])) == keccak256(abi.encodePacked(candidate))) {
return true;
}
}
return false;
}
한 가지 짚고 넘어가야할 것은 스마트 컨트랙트에서는 string 비교를 할 수 없다는 사실이다. 따라서 candidateList 배열 안에 입력 받은 후보자와 일치하는 후보자가 있는지 여부를 체크할 때 keccak256( ) 메소드를 사용해 string 값을 해싱하여 16진수로 변환한 뒤 서로의 해시값을 비교하는 방식을 채택하게 된다. 스마트 컨트랙트 안에서 string 값의 비교를 처리할 때 자주 사용되는 방법이므로 숙지하고 넘어가도록 하자.
이제 후보자에 대한 투표 기능을 갖고 있는 voteForCandidate( ) 함수를 다음과 같이 만들어주었다.
function voteForCandidate(string memory candidate) public {
// 후보군에 없는 사람의 경우 실행 종료
require(validCandidate(candidate), "Error !!");
votesReceived[candidate] += 1;
}
후보자(candidate)를 인자값으로 받고 있으며 require( ) 구문 안의 validCandidate( ) 함수를 통해 candidateList 배열 안에 인자값으로 전달받은 후보자가 존재할 경우에만 해당 함수가 실행되게끔 하였다. 그리고 만약 candidateList 배열 안에 입력받은 후보자가 존재한다면 votesReceived[candidate] += 1 을 통해 해당 후보자의 득표수를 +1 증가시켜주었다.
마지막으로 특정 후보자의 전체 득표수를 조회할 수 있는 함수를 다음과 같이 만들어주었다.
function totalVotesFor(string memory candidate) public view returns (uint8) {
require(validCandidate(candidate), "Error !!");
return votesReceived[candidate];
}
totalVotesFor( ) 함수 역시 voteForCandidate( ) 함수와 마찬가지로 require( ) 구문 안의 validCandidate( ) 함수를 통해 candidateList 배열 안에 인자값으로 전달받은 후보자가 존재할 경우에만 함수가 실행되게끔 하였다. return 값으로는 votesReceived[ candidate ] 을 반환하여 특정 후보자의 전체 득표수를 조회 가능하도록 만들었다.
스마트 컨트랙트로 만들어본 투표 Dapp의 전체 코드는 다음과 같다.
/* Voting.sol 파일 */
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
contract Voting {
string[] public candidateList;
mapping(string => uint8) public votesReceived;
// 컨트랙트 배포 시점에서 candidateList에 값 할당.
constructor(string[] memory candidateNames) {
candidateList = candidateNames;
}
function voteForCandidate(string memory candidate) public {
// 후보군에 없는 사람일 경우 실행 종료
require(validCandidate(candidate), "Error !!");
votesReceived[candidate] += 1;
}
// 특정 후보자의 전체 득표수 확인
function totalVotesFor(string memory candidate) public view returns (uint8) {
require(validCandidate(candidate), "Error !!");
return votesReceived[candidate];
}
function validCandidate(string memory candidate) private view returns (bool) {
// 1. 후보자 리스트 : candidateList
// 2. candidateList 안에 입력한 후보자와 일치하는 후보자가 있는지 검증
for (uint i = 0; i < candidateList.length; i++) {
if (keccak256(abi.encodePacked(candidateList[i])) == keccak256(abi.encodePacked(candidate))) {
return true;
}
}
return false;
}
}
truffle migration을 이용해 배포를 진행할 때는 다음과 같이 deployer.deploy( ) 의 두번째 인자값으로 후보자들을 담은 배열을 전달해주도록 하자.
/* 2_deploy_Voting.js 파일 */
const Voting = artifacts.require('Voting');
module.exports = function (deployer) {
deployer.deploy(Voting, ['둘리', '도우너', '또치', '마이클']);
};
이제 테스트 코드를 작성해 스마트 컨트랙트가 제대로 동작하는지 확인해주기만 하면 된다.
const Voting = artifacts.require('Voting');
// only를 붙일 경우 해당 테스트 코드만 실행됨
describe.only('Voting', () => {
let deployed;
let candidateList;
it('deployed', async () => {
deployed = await Voting.deployed();
// console.log(deployed);
});
it('candidateList', async () => {
// const candidate1 = await deployed.candidateList.call(0); // 1초
// const candidate2 = await deployed.candidateList.call(1); // 1초
// const candidate3 = await deployed.candidateList.call(2); // 1초
// const candidate4 = await deployed.candidateList.call(3); // 1초
// 총 4초 소요
// 이더리움 네트워크에서는 배열 전체를 한번에 가져오는게 불가능하다.
// 배열 안의 요소 하나하나에 대해 요청을 보내 값을 가져와야 한다.
const request = [
deployed.candidateList.call(0), // 1초
deployed.candidateList.call(1), // 2초
deployed.candidateList.call(2), // 3초
deployed.candidateList.call(3), // 1초
];
// 총 3초 소요
// Promise로 반환되는 값들을 배열 안에 담는다.
candidateList = await Promise.all(request);
// 전부 백그라운드에 넣어놓고 완료되는 순서대로 테스트 큐에 쌓이면서 콜스택으로 넘어오게 된다.
console.log(candidateList);
});
it('voteForCandidate', async () => {
await deployed.voteForCandidate(candidateList[0]);
await deployed.voteForCandidate(candidateList[1]);
await deployed.voteForCandidate(candidateList[3]);
await deployed.voteForCandidate(candidateList[2]);
await deployed.voteForCandidate(candidateList[0]);
await deployed.voteForCandidate(candidateList[0]);
await deployed.voteForCandidate(candidateList[2]);
await deployed.voteForCandidate(candidateList[0]);
for (const candidate of candidateList) {
let count = await deployed.totalVotesFor(candidate);
console.log(`${candidate} : ${count}`);
}
});
});
참고로 스마트 컨트랙트 안에서 함수를 선언할 때 view 함수 , pure 함수와 같은 형태로 함수가 선언되는 것을 볼 수 있는데 각각이 의미하는 바는 다음과 같다.
- view 함수 : 함수 안에서 상태변수를 사용한다. 하지만 상태변수의 값을 바꾸지는 않는다. 상태변수를 return 하는 경우에도 view 함수에 해당한다.
- pure 함수 : 함수 안에서 상태변수를 사용하지 않으며 상태변수의 값 역시 바꾸지 않는다. pure 함수는 컨트랙트 안의 어떠한 데이터에도 접근하지 않는다.
- view 혹은 pure 를 명시하지 않는 경우 : 함수 안에서 상태변수를 사용하며 상태변수의 값을 변경하는 함수.
'Ethereum' 카테고리의 다른 글
Ethereum/이더리움 - 인터페이스 & ERC-20 / 토큰 발행하기 (0) | 2022.07.22 |
---|---|
Ethereum/이더리움 - Solidity(솔리디티) function payable (0) | 2022.07.21 |
Ethereum/이더리움 - 스마트 컨트랙트로 토큰 발행하기 (0) | 2022.07.17 |
Ethereum/이더리움 - 스마트 컨트랙트 이벤트 등록 및 백엔드에서 트랜잭션 생성하기 (0) | 2022.07.14 |
Ethereum/이더리움 - 메타마스크를 통한 스마트 컨트랙트 실행 (0) | 2022.07.13 |