체인의정석

Tally와 호환되는 DAO 컨트렉트 (timelock & erc20) 구현하기 본문

블록체인/Solidity

Tally와 호환되는 DAO 컨트렉트 (timelock & erc20) 구현하기

체인의정석 2024. 8. 9. 10:41
728x90
반응형

0. Open zepplin wizard

총 3개의 컨트렉트가 필요하다.

- 오픈제플린 위자드에서 보면 DAO 중 ERC20토큰 중 ERC20 Vote (이건  투표용 토큰이 된다)

- DAO코드 내부의 TimelockController가 이용할 TimelockController (이건 트레져리가 된다.)

- 위 2개의 설정을 이어받은 DAO (이게 DAO 컨트렉트이다.)

모두 코드들이 제공되기 때문에 쉽게 구할 수 있다.

https://wizard.openzeppelin.com/#erc20

 

OpenZeppelin Contracts Wizard

An interactive smart contract generator based on OpenZeppelin Contracts.

wizard.openzeppelin.com

참고로 해당 내용을 자세히 알기 위해서는 아래 페이지를 참고하면 된다.

https://docs.openzeppelin.com/contracts/5.x/governance

 

How to set up on-chain governance - OpenZeppelin Docs

Decentralized protocols are in constant evolution from the moment they are publicly released. Often, the initial team retains control of this evolution in the first stages, but eventually delegates it to a community of stakeholders. The process by which th

docs.openzeppelin.com

 

1. Time lock의 생성자 순서

DAO를 만들 때 proposal을 바로 실행하는 것이 아니라 timelock을 통해서 특정 시간 이후에 실행하는 것이 일반적이다.

이번에는 실행 전에 AI를 써서 여론 분석을 진행하고 실행을 멈출 수 있는 구조를 구현하기 위함이므로 투표종료 후에 분석할 시간을 주고나서 실행 전에 중지시키는 구조로 진행하였다.

https://docs.openzeppelin.com/contracts/4.x/governance

 

How to set up on-chain governance - OpenZeppelin Docs

Decentralized protocols are in constant evolution from the moment they are publicly released. Often, the initial team retains control of this evolution in the first stages, but eventually delegates it to a community of stakeholders. The process by which th

docs.openzeppelin.com

오픈제플린 공식 예제가 있기 때문에 이는 어려운 일은 아니지만 햇갈리는 부분이 있었다.

바로 생성자에 들어가는 proposers와 executors 다.

여기서 porposer는 DAO 컨트렉트여야 한다. 실제로 오픈제플린 위자드에서 만들어준 코드를 보면 실행 부분을 timelock이 해주도록 생성이 된다. 

// DAO 코드에서 안건 제안하는 부분에서 Timelock의 scheduleBatch 호출
    /**
     * @dev Function to queue a proposal to the timelock.
     */
    function _queueOperations(
        uint256 proposalId,
        address[] memory targets,
        uint256[] memory values,
        bytes[] memory calldatas,
        bytes32 descriptionHash
    ) internal virtual override returns (uint48) {
        uint256 delay = _timelock.getMinDelay();

        bytes32 salt = _timelockSalt(descriptionHash);
        _timelockIds[proposalId] = _timelock.hashOperationBatch(targets, values, calldatas, 0, salt);
        _timelock.scheduleBatch(targets, values, calldatas, 0, salt, delay);

        return SafeCast.toUint48(block.timestamp + delay);
    }

// Proposer가 실행하는 함수 - 스케쥴
    function scheduleBatch(
        address[] calldata targets,
        uint256[] calldata values,
        bytes[] calldata payloads,
        bytes32 predecessor,
        bytes32 salt,
        uint256 delay
    ) public virtual onlyRole(PROPOSER_ROLE) {
        if (targets.length != values.length || targets.length != payloads.length) {
            revert TimelockInvalidOperationLength(targets.length, payloads.length, values.length);
        }
        
//DAO 컨트렉트에서 Timelock의 executeBatch 실행
    function executeBatch(
        address[] calldata targets,
        uint256[] calldata values,
        bytes[] calldata payloads,
        bytes32 predecessor,
        bytes32 salt
    ) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) {
        if (targets.length != values.length || targets.length != payloads.length) {
            revert TimelockInvalidOperationLength(targets.length, payloads.length, values.length);
        }

        bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt);

        _beforeCall(id, predecessor);
        for (uint256 i = 0; i < targets.length; ++i) {
            address target = targets[i];
            uint256 value = values[i];
            bytes calldata payload = payloads[i];
            _execute(target, value, payload);
            emit CallExecuted(id, i, target, value, payload);
        }
        _afterCall(id);
    }

    
// Executor가 실행하는 함수 - 실행
  function executeBatch(
        address[] calldata targets,
        uint256[] calldata values,
        bytes[] calldata payloads,
        bytes32 predecessor,
        bytes32 salt
    ) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) {
        if (targets.length != values.length || targets.length != payloads.length) {
            revert TimelockInvalidOperationLength(targets.length, payloads.length, values.length);
        }

위에서 볼 수 있듯이 크게 제안과 실행 부분에서 timelock의 role이 제한으로 걸리게 되는데

제안의 경우는 사실상 DAO 컨트렉트에서만 호출이 되게 하는게 맞지만 실행의 경우에는 사실 안건도 다 통과 된것이고 기간도 다 지나고 실행시키는것이라 누가 실행해도 문제가 없다. 물론 관리자만 실행하게 해도 된다.

아무튼 이런 형태로 초기 생성자가 꼬이기 때문에 나는 timeLock을 상속받으면서 권한 설정을 해주는 grantAllRole 함수를 하나 더 추가하였다. 원래는 create2를 써서 순서를 안꼬이게 하려 했지만 해당 방법으로 하다보면 각 컨트렉트의 생성자가 서로의 주소를 쓰기 때문에 어쩔 수 없이 모든 컨트렉트를 배포한 후에 추가적인 함수 실행이 필요했다. 왜 이렇게했는가 생각해보면 오픈제플린 입장에서는 DAO, Timelock 두 컨트렉트가 서로 범용적으로 사용되고 활용방안이 다양하기 때문에 모든 경우의 수를 열어두는 것으로 보인다.

// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.0.0
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/governance/TimelockController.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract DaaoTimelock is TimelockController, Ownable {
    constructor(
        uint256 minDelay,
        address[] memory proposers,
        address[] memory executors,
        address admin
    )
    Ownable(msg.sender)
    TimelockController(minDelay,proposers,executors,admin)
    {

    }

    function grantAllRole(address grantedAddress) public onlyOwner {
        super._grantRole(PROPOSER_ROLE, grantedAddress);
        super._grantRole(CANCELLER_ROLE, grantedAddress);
        super._grantRole(EXECUTOR_ROLE, grantedAddress);
    }
}

또한 추가적인 체크로직의 경우에는 실행함수에 require문을 달면 되기 때문에 함수 자체가 호출에 실패한다. 그래서 원하는 쉽게 작성할 수 있었다. checkAIPass가 이에 해당된다.

    function _executeOperations(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
        internal
        override(Governor, GovernorTimelockControl)
    {
        require(checkAIPass(proposalId), "DAAO: AI paused execution");
        super._executeOperations(proposalId, targets, values, calldatas, descriptionHash);
    }

2. Tally에 배포하기

웬만한 DAO의 UI는 Tally를 쓴다.
https://www.tally.xyz/

 

Tally | Start, join, and grow DAOs

Tally is a DAO operations platform. DAOs use Tally to create and pass proposals, enable delegation, and power voting.

www.tally.xyz

Tally 등록을 위해서는 Openzepplin Governor를 사용하면 된다.
참고로 당연하게도 인터페이스 정도만 맞추면 알아서 잘 작동하니 원하는 추가로직이 있다면 그 부분만 오버라이딩 해서 쓰면된다.

내가 진행한 첫날에는 오류가 나서 UI가 멈췄지만 둘째날 공교롭게도 오류가 해결되었다. (아비트럼 재단에서 돈을 주면서 외주를 맡겼다는 소식을 듣긴했는데 그것 때문일 수도 ㅋㅋ) 아무튼 이제 작동도 잘하고 탈리의 UI는 너무 잘 되어 있어서 따로 UI를 만들 필요가 없다.


Tally를 쓸때는 Add a DAO -> Deploy myself 누르고 자신이 배포한 컨트렉트 주소와 배포된 블록번호를 각각 배포된 상황에 맞게 올리면된다. 만약 이것도 좀 더 편하게 쓰려면 verify를 진행하면 된다.
참고로 나는 block scout에 verify 했는데 하는 방법은 아래 깃허브 리드미에 다 정리해 두었다.

3. 소스코드

https://github.com/hyunkicho/DAAO_Tally

 

GitHub - hyunkicho/DAAO_Tally: test contracts for dao & tally

test contracts for dao & tally. Contribute to hyunkicho/DAAO_Tally development by creating an account on GitHub.

github.com

 

728x90
반응형
Comments