본문 바로가기

분석/블록체인

[Ethereum] Governor에 대해 알아보자

OpenZeppelin Contract

Governor

추상 contract로 핵심이 되는 로직을 담고 있다.

 

안건은 아래처럼 저장된다.

투표가 언제 시작되고 언제 끝나는지와

실행 유무와 취소 유무를 저장한다.

struct ProposalCore {
    Timers.BlockNumber voteStart;
    Timers.BlockNumber voteEnd;
    bool executed;
    bool canceled;
}

mapping(uint256 => ProposalCore) private _proposals;

 

아래는 안건을 제안하는 로직이다.

1. targets, values, calldatas, description을 사용해서 안건의 식별키인 proposalId를 만든다.

2. 안건에 대한 투표가 시작되는 block number인  snapshot과 투표가 마감되는 block number인 deadline을 정한다.

function propose(
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    string memory description
) public virtual override returns (uint256) {
    require(
        getVotes(_msgSender(), block.number - 1) >= proposalThreshold(),
        "Governor: proposer votes below proposal threshold"
    );

    uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description)));

    require(targets.length == values.length, "Governor: invalid proposal length");
    require(targets.length == calldatas.length, "Governor: invalid proposal length");
    require(targets.length > 0, "Governor: empty proposal");

    ProposalCore storage proposal = _proposals[proposalId];
    require(proposal.voteStart.isUnset(), "Governor: proposal already exists");

    uint64 snapshot = block.number.toUint64() + votingDelay().toUint64();
    uint64 deadline = snapshot + votingPeriod().toUint64();

    proposal.voteStart.setDeadline(snapshot);
    proposal.voteEnd.setDeadline(deadline);

    emit ProposalCreated(
        proposalId,
        _msgSender(),
        targets,
        values,
        new string[](targets.length),
        calldatas,
        snapshot,
        deadline,
        description
    );

    return proposalId;
}

 

아래는 투표를 하는 로직이다.

1. proposalId로 _proposals를 조회한다.

2. _getVotes 함수를 사용해서 투표권을 얼마큼 위임받았는지를 나타내는 weight을 알아낸다.

3. _countVote 함수를 사용해서 어디에 투표했는지를 기록한다.

function _castVote(
    uint256 proposalId,
    address account,
    uint8 support,
    string memory reason,
    bytes memory params
) internal virtual returns (uint256) {
    ProposalCore storage proposal = _proposals[proposalId];
    require(state(proposalId) == ProposalState.Active, "Governor: vote not currently active");

    uint256 weight = _getVotes(account, proposal.voteStart.getDeadline(), params);
    _countVote(proposalId, account, support, weight, params);

    if (params.length == 0) {
        emit VoteCast(account, proposalId, support, weight, reason);
    } else {
        emit VoteCastWithParams(account, proposalId, support, weight, reason, params);
    }

    return weight;
}

 

아래는 투표가 마감되고 실행하기로 결정된 안건을 실행하는 로직이다.

1. targets, values, calldatas, description을 사용해서 안건의 식별키인 proposalId를 만든다.

2. proposalId로 안건의 state를 조회해서 제안 상태가 성공했는지를 확인한다.

3. 반복문을 돌면서 calldatas 안에 담긴 함수 호출을 실행한다.

function execute(
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    bytes32 descriptionHash
) public payable virtual override returns (uint256) {
    uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash);

    ProposalState status = state(proposalId);
    require(
        status == ProposalState.Succeeded || status == ProposalState.Queued,
        "Governor: proposal not successful"
    );
    _proposals[proposalId].executed = true;

    emit ProposalExecuted(proposalId);

    _beforeExecute(proposalId, targets, values, calldatas, descriptionHash);
    _execute(proposalId, targets, values, calldatas, descriptionHash);
    _afterExecute(proposalId, targets, values, calldatas, descriptionHash);

    return proposalId;
}


function _execute(
    uint256,
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    bytes32 
) internal virtual {
    string memory errorMessage = "Governor: call reverted without message";
    for (uint256 i = 0; i < targets.length; ++i) {
        (bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]);
        Address.verifyCallResult(success, returndata, errorMessage);
    }
}

 

아래는 안건의 state를 조회하는 로직이다.

1. proposalId로 _proposals를 조회한다.

2. 현재 block number와 투표 시작을 나타내는 snapshot을 비교한다. 

3. 현재 block number와 투표 마감을 나타내는 deadline을 비교한다.

4. _quorumReached 함수와 _voteSucceeded 함수를 통해 정족수를 넘겼는지와 투표가 성공했는지를 알아낸다. 

function state(uint256 proposalId) public view virtual override returns (ProposalState) {
    ProposalCore storage proposal = _proposals[proposalId];

    if (proposal.executed) {
        return ProposalState.Executed;
    }

    if (proposal.canceled) {
        return ProposalState.Canceled;
    }

    uint256 snapshot = proposalSnapshot(proposalId);

    if (snapshot == 0) {
        revert("Governor: unknown proposal id");
    }

    if (snapshot >= block.number) {
        return ProposalState.Pending;
    }

    uint256 deadline = proposalDeadline(proposalId);

    if (deadline >= block.number) {
        return ProposalState.Active;
    }

    if (_quorumReached(proposalId) && _voteSucceeded(proposalId)) {
        return ProposalState.Succeeded;
    } else {
        return ProposalState.Defeated;
    }
}

 

GovernorVote

Governor를 상속하는 추상 contract이다.

ERC-20 또는 ERC-721 token을 사용해서 투표권을 얼마큼 가지고 있는지를 나타내는 voting weight을 알아낼 수 있다.

 

생성자로 token을 받는다. 

IVotes public immutable token;

constructor(IVotes tokenAddress) {
    token = tokenAddress;
}

 

아래는 token을 사용해서 투표권을 얼마큼 가지고 있는지를 반환한다.

function _getVotes(
    address account,
    uint256 blockNumber,
    bytes memory 
) internal view virtual override returns (uint256) {
    return token.getPastVotes(account, blockNumber);
}

 

GovernorCountingSimple

Governor를 상속하는 추상 contract이다.

 

투표는 찬성을 나타내는 for, 반대를 나타내는 against, 기권을 나타내는 abstain으로 나뉜다.

투표를 각 분류별로 얼마큼 했는지를 아래처럼 저장한다.

enum VoteType {
    Against,
    For,
    Abstain
}

struct ProposalVote {
    uint256 againstVotes;
    uint256 forVotes;
    uint256 abstainVotes;
    mapping(address => bool) hasVoted;
}

mapping(uint256 => ProposalVote) private _proposalVotes;

 

아래는 정족수를 넘겼는지와 투표가 성공했는지를 알아내는 로직이다.

function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
    ProposalVote storage proposalVote = _proposalVotes[proposalId];

    return quorum(proposalSnapshot(proposalId)) <= proposalVote.forVotes + proposalVote.abstainVotes;
}

function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
    ProposalVote storage proposalVote = _proposalVotes[proposalId];

    return proposalVote.forVotes > proposalVote.againstVotes;
}

 

아래는 투표를 어디에 했는지 기록하는 로직이다.

1. proposalId로 안건에 대한 투표 기록을 조회한다.

2. account가 이미 투표를 했는지 확인한다.

3. support가 찬성인지 반대인지 기권인지에 따라 투표기록을 남긴다.

function _countVote(
    uint256 proposalId,
    address account,
    uint8 support,
    uint256 weight,
    bytes memory 
) internal virtual override {
    ProposalVote storage proposalVote = _proposalVotes[proposalId];

    require(!proposalVote.hasVoted[account], "GovernorVotingSimple: vote already cast");
    proposalVote.hasVoted[account] = true;

    if (support == uint8(VoteType.Against)) {
        proposalVote.againstVotes += weight;
    } else if (support == uint8(VoteType.For)) {
        proposalVote.forVotes += weight;
    } else if (support == uint8(VoteType.Abstain)) {
        proposalVote.abstainVotes += weight;
    } else {
        revert("GovernorVotingSimple: invalid value for enum VoteType");
    }
}