투표 스마트 컨트랙트( remix, hardhat)

김진경·2022년 5월 2일
1

Solidity

목록 보기
2/2

가장 대표적인 스마트 컨트랙트 예제, 투표(Ballot) 스마트 컨트랙트를 작성하고 분석해보았다. RemixHardhat, 리액트를 사용하여 테스트를 하고 로컬에서 배포까지 진행했다.


1. 스마트 컨트랙트 작성

투표는 먼저 안건을 제출한 뒤, 제출된 안건을 바탕으로 스마트 컨트랙트를 작성한다.

먼저, 안건을 제출하는 스마트 컨트랙트 코드이다.

// SPDX-License-Identifier: MIT Liscense
pragma solidity >=0.7.0 <0.9.0;
//  위임을 전달받기 위해선 해당 계정은 투표권리를 받은 상태여야 한다.
// 위임을 하면 위임받은 계정이 투표한 제안에 자동으로 투표된다.
contract getBytes {
    function getBytesNow1() pure public returns (bytes32[1] memory ba1) 
    {
        ba1 = [bytes32("Proposal A")];
    }
    function getBytesNow2() pure public returns (bytes32[1] memory ba2) 
    {
        ba2 = [bytes32("Proposal B")];
    }
    function getBytesNow3() pure public returns (bytes32[1] memory ba3) 
    {
        ba3 = [bytes32("Proposal C")];
    }
}

위의 함수들은 각각 안건을 1개씩 발생시킨다.

발생한 안건들을 proposalNames 라는 bytes32[] 형태로
아래 투표 컨트랙트에 대입하여 배포한다.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;

//create a smart contract call Ballot
contract Ballot {

    //This struct function is a variable that contains many features
    //define a voter in this ballot

    struct Voter {
        uint weight; // weight는 투표권. 의장에 의해 주어진다.
        bool voted;  // 만약 이 값이 true라면, 그 사람은 이미 투표한 것 입니다.
        address delegate; // 투표에 위임된 사람
        uint vote; // 투표된 제안의 인덱스 데이터 값
    }
    // 이것은 단일 제안에 대한 유형.
    struct Proposal {
        bytes32 name; // 간단한 명칭. 
        uint voteCount; // 누적 투표수. 
    }


    address public chairperson;
    // 각각의 주소에 대해 `Voter` 구조체를 저장하는 상태변수를 선언한다.
    mapping (address => Voter) public voters;

    // 동적으로 크기가 지정된 `Proposal` 구조체의 배열입니다.
    Proposal[] public proposals;

    // 안건을 인자로 받아 컨트랙트를 초기화한다.
    constructor(bytes32[] memory proposalNames) {
        chairperson = msg.sender;
        voters[chairperson].weight = 1;
        // 안건을 추가할때마다, 새로운 제안서 개체를 만들어 배열 끝에 추가한다.
        for(uint i = 0; i < proposalNames.length; i++) {
            proposals.push(Proposal({
                name: proposalNames[i],
                voteCount:0
            }));
        }
    }

    // `voter` 에게 이 투표권에 대한 권한을 부여하십시오.
    // 오직 `chairperson` 으로부터 호출받을 수 있습니다.
    function giveRighttoVote (address voter) external {
        require(
            msg.sender == chairperson, // 투표권을 주는 건 의장만 가능
            "Only Chairperson allowed to assign voting rights."
        );

        require(
            !voters[voter].voted, // 만약 voters[voter].voted 이 참이면, !에 의해 false가 되기에 require를 통과하지 못함. 결과적으로 투표한 적이 없어야 함.
            "Voter already voted once."
        );
        require(voters[voter].weight == 0); // 투표권이 없어야 함.
        // 위의 조건들을 다 충족하고 나면 투표권을 1개 준다.
        voters[voter].weight = 1;
    }


    function removeVotingRights(address voter) external {
        require(msg.sender == chairperson, "Only Chairperson allowed to remove voting rights.");
         // voters[voter].voted가 true 인 경우는 투표권리를 삭제 못하게 require문 작성. 만약 투표를 안해서 voters[voter].voted가 false면, !false는 true가 되서 require문 통과.
        require(!voters[voter].voted, "Voter cannot be removed while vote is active");
        require(voters[voter].weight == 1); // weight가 남아있어야함.
        voters[voter].weight = 0; // 투표권 박탈.
    }


    
    // `to` 로 유권자에게 투표를 위임하십시오.
    function delegate(address to) external {
        Voter storage sender = voters[msg.sender];
        require(!sender.voted, "You already voted once."); // 만약 sender.voted가 참(이미 투표했다는 말)이면 !에 의해 false가 되어 require문 통과 X

        require(to != msg.sender, "Self-delegation is not allowed"); // 자체 위임은 허용되지 않습니다.

        // `to`가 위임하는 동안 delegation을 전달하십시오.
        // 일반적으로 이런 루프는 매우 위험하기 때문에,
        // 너무 오래 실행되면 블록에서 사용가능한 가스보다
        // 더 많은 가스가 필요하게 될지도 모릅니다.
        // 이 경우 위임(delegation)은 실행되지 않지만,
        // 다른 상황에서는 이러한 루프로 인해
        // 스마트 컨트랙트가 완전히 "고착"될 수 있습니다.
        while (voters[to].delegate != address(0)) { // voters[to].delegate가 공백(address(0))이 아니라면... 이라는 조건문
        // https://bbokkun.tistory.com/166
        // https://stackoverflow.com/questions/48219716/what-is-address0-in-solidity
            to = voters[to].delegate;
            require(to != msg.sender, "Found loop during Delegation"); // 우리는 delegation에 루프가 있음을 확인 했고 허용하지 않았습니다.
        }

        sender.voted = true; // msg.sender.voted가 true로 된건데 위임을 했으니까 더 이상 투표권이 없다는 뜻.
        sender.delegate = to; // msg.sender.delegeate에 인자(투표권을 준 주소)를 대입
        Voter storage delegate_ = voters[to]; //delegate_라는 변수에 to 라는 주소의 Voter 구조체 할당.
        if(delegate_.voted){// 대표가 이미 투표한 경우, 투표 수에 직접 추가 하십시오
            proposals[delegate_.vote].voteCount += sender.weight; // proposals라는 구조체 배열의 인덱스(delegate_.vote)는 안건(Proposal 구조체)를 의미한다. 투표한 해당 안건의 voteCount에 sender.weight를 더한다.
        } else { // 대표가 아직 투표하지 않았다면 weight에 추가하십시오.
            delegate_.weight += sender.weight;   // 위임하는 사람(주소)의 투표권 수를 위임받는 사람(주소)의 투표권 수에 더한다.
        }
    }

    function vote(uint proposal) external { // 위의 delegate 함수와 동일
        Voter storage sender = voters[msg.sender];
        require(sender.weight != 0, "No right to vote");
        require(!sender.voted, "Already voted once.");
        sender.voted = true;
        sender.vote = proposal;

        proposals[proposal].voteCount += sender.weight;
    }

    // 모든 이전 득표를 고려하여 승리한 제안서를 계산합니다. 
    function winningProposal() public view returns (uint winningProposal_) {
        uint winningVoteCount = 0;
        for(uint p = 0; p < proposals.length; p++) { // 안건 배열을 조회.
            if(proposals[p].voteCount > winningVoteCount) { // 첫 안건부터 투표받은 수를 조회하여
                winningVoteCount = proposals[p].voteCount; // 가장 많은 투표 수를 갱신하고
                winningProposal_ = p; // 받은 안건도 계속 갱신한다.
            }
        }
    }

    function winnerName() external view returns (bytes32 winnerName_) {
        winnerName_ = proposals[winningProposal()].name; // 가장 많은 득표를 한 안건을 반환
    }

}

2. Remix IDE 테스트

테스트는 다음과 같은 순서로 진행된다.

  1. 안건(Proposals) 제출
  2. 의장(chairperson)의 투표권 부여
  3. 참여자(Account)들의 투표.

먼저 안건을 제출하여 bytes32 데이터를 확인한다.

그리고 의장이 참여자(계정)에게 투표권을 부여한다.

투표권을 얻은 참여자들이 안건에 대해 투표를 진행한다.
(1번에 투표를 했고, proposals에서 1번 안건의 'voteCount' 가 1 증가한 것을 확인할 수 있다)


3. Hardhat 로컬 네트워크 배포

추가로 하드햇으로 로컬에서도 컨트랙트 배포를 진행해보았다.

  1. npx hardhat node로 노드 실행.

  2. 배포 명령어 입력
    npx hardhat run --network <your-network> scripts/deploy.js

    node가 실행되고 있는 화면에서 배포된 주소를 확인할 수 있다.



Review

솔리디티 코드를 처음으로 완전히 분석해봤다. 한가지 느낀 것은, 테스트를 하면서 좀 더 로직이 돌아가는 흐름을 좀 더 확실하게 알게 되었다는 것이다. 코드를 작성하고 리뷰를 하면서 거의 다 알게 되지만, 테스트를 하면서 좀 더 확실하게 스스로가 한 분석에 대한 확신을 갖게 되었다.

또 하드햇을 사용하면서 하드햇 = 트러플 + 가나슈 라는 느낌을 받았다. 테스트 계정과 배포까지 가능한 것에 대해 상당히 편리한 느낌을 받았다. 타입스크립트와의 호환성이 뛰어나다는 평가가 있던데, 나중에 타입스크립트와 조합하여 dApp을 만들어보는 것도 좋겠다.

profile
Maktub.

0개의 댓글