ENS DAO治理流程
May 17th, 2022

ENS采用了社区DAO治理,ENS token数量可以委托给投票人,当投票人的被委托数量超过10万个后可以在社区提出提案,持有ENS投票数的地址可以对提案进行同意/弃权/不同意的投票,在7天的投票期结束后,如果同意数超过了不同意数且同意和弃权的数量超过了总可投票数的1%,就视提案为成功。任何地址可以将此提案改为排队中状态,进入2天的锁定期,在这锁定期内,有不同意此提案的地址可以做相应的交易操作,免去提案被执行后的影响,过了2天锁定期后,任何地址可以执行此提案,从而提案就完成了。

核心概念

  • ENS- ERC20代币,用于委托给指定用户投票。

  • Delegation委托 — ENS持有人只有在将投票权委托给某个地址后才能投票或创建提案,创建提案需要被委托超过10万个。 一个地址只能委托给一个地址,可委托给自己的地址,委托时会将自己地址的所有ENS余额数量都委托出去。用了Checkpoint这个结构来存储委托的票数,fromBlock表示从哪个区块开始委托,因此在提案投票时,计算某地址的可投票数是根据提完在开始投票时的区块高度来算的(此方式可以防止某些地址在投票开始来拉票的行为)。委托地址转移代币时也将自动委托给目标地址,也可以重新委托给其它地址。

    struct Checkpoint {
        uint32 fromBlock;
        uint224 votes;
    }
    
  • Proposals提案 — 实际上是一串交易参数的数组,包括目标合约地址target,发送交易的eth数量value,调用函数及参数calldata,描述description。提案上链后会存放在Governor的_proposals变量里。ENS的提案并没有开放cancel功能。

    function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
    
  • Voting投票 — 在提案开始投票区块前拥有委托票数的地址可以对提案进行投票。有不同意,同意,弃权三种投票选项。投票可以使用 castVote 立即提交,也可以使用 castVoteBySig 离线签名稍后提交。 在投票结束后,如果同意数大于不同意数,且同意加弃权票数大于等于1%的总委托票数,就表示提案投票成功。

    enum VoteType {
        Against,
        For,
        Abstain
    }
    function castVote(uint256 proposalId, uint8 support)
    function castVoteBySig(
      uint256 proposalId,
      uint8 support,
      uint8 v,
      bytes32 r,
      bytes32 s
    ) 
    
  • Timelock时间锁 — 所有治理和其他管理操作都必须在时间锁中停留至少2天,然后才能在协议中实施。投票成功的提案,可以调用queue方法进入锁定期,锁定期结束后就可以调用execute执行提案。

    function queue(
      address[] memory targets,
      uint256[] memory values,
      bytes[] memory calldatas,
      bytes32 descriptionHash
    )
    function execute(
      address target,
      uint256 value,
      bytes calldata data,
      bytes32 predecessor,
      bytes32 salt
    )
    

    实例

    下面以EP11为例,解析一下从提案到执行的全过程。

    https://docs.ens.domains/v/governance/governance-proposals/ep11-executable-end-airdrop

    可以看到此提案是对之前空投剩余未认领的代币进行sweep操作,全部领取到参数地址,然后对认领空投的合约取消TimelockController合约的ENS代币授权。

1. 提出提案

function propose(
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    string memory description
) public virtual override returns (uint256) {
    uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description)));//生成keccak256哈希做为提案ID

    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();//开始投票区块=当前区块+投票等待期(1区块)
    uint64 deadline = snapshot + votingPeriod().toUint64();//结束投票区块=开始投票区块+投票持续期(7天)

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

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

    return proposalId;
}

主要是根据传入的执行参数生成提案ID,然后保持到storage变量里,再根据系统参数设置投票开始和结束区块,并发出提案创建事件。

2. 投票

function castVote(uint256 proposalId, uint8 support) public virtual override returns (uint256) {
    address voter = _msgSender();
    return _castVote(proposalId, voter, support, "");
}
function _castVote(
    uint256 proposalId,
    address account,
    uint8 support,
    string memory reason
) 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());//获取此地址的可投票数
    _countVote(proposalId, account, support, weight);//累积票数

    emit VoteCast(account, proposalId, support, weight, reason);//发出投票事件

    return weight;
}
function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) {
    return token.getPastVotes(account, blockNumber);//获取地址在指定区块的委托票数
}
function _countVote(
    uint256 proposalId,
    address account,
    uint8 support,
    uint256 weight
) 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");//无效投票选项,revert
    }
}

投票时会根据提案的开始投票区块取得用户地址的可投票数,然后将所有可投票数累积到投票选项的总票数里,不支持投部分数量的票。

3. 排队

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

    require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful");//提案必须的成功状态才可以排队

    uint256 delay = _timelock.getMinDelay();//锁定时间
    _timelockIds[proposalId] = _timelock.hashOperationBatch(targets, values, calldatas, 0, descriptionHash);//计算keccak256哈希做为提案ID
    _timelock.scheduleBatch(targets, values, calldatas, 0, descriptionHash, delay);//提案可执行交易批量排期

    emit ProposalQueued(proposalId, block.timestamp + delay);//发出提案排队事件,包括ID和锁定结束时间戳

    return proposalId;
}
function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) {
    ProposalState status = super.state(proposalId);

    if (status != ProposalState.Succeeded) {
        return status;
    }//提案不是投票成功状态,就直接返回

    // core tracks execution, so we just have to check if successful proposal have been queued.
    bytes32 queueid = _timelockIds[proposalId];
    if (queueid == bytes32(0)) {
        return status;
    } else if (_timelock.isOperationDone(queueid)) {
        return ProposalState.Executed;
    } else {
        return ProposalState.Queued;
    }
}
function scheduleBatch(
    address[] calldata targets,
    uint256[] calldata values,
    bytes[] calldata datas,
    bytes32 predecessor,
    bytes32 salt,
    uint256 delay
) public virtual onlyRole(PROPOSER_ROLE) {
    require(targets.length == values.length, "TimelockController: length mismatch");
    require(targets.length == datas.length, "TimelockController: length mismatch");

    bytes32 id = hashOperationBatch(targets, values, datas, predecessor, salt);
    _schedule(id, delay);//排期
    for (uint256 i = 0; i < targets.length; ++i) {
        emit CallScheduled(id, i, targets[i], values[i], datas[i], predecessor, delay);//发出提案排期事件
    }
}
function _schedule(bytes32 id, uint256 delay) private {
    require(!isOperation(id), "TimelockController: operation already scheduled");
    require(delay >= getMinDelay(), "TimelockController: insufficient delay");//锁定时间必须大于等于配置的最小锁定时间
    _timestamps[id] = block.timestamp + delay;//设置提案的锁定结束时间戳
}

排队就是将提案的target,value,calldata等参数批量传入,生成时间锁的提案ID,并分别设置可执行交易的锁定结束时间。

4. 执行

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);

    _execute(proposalId, targets, values, calldatas, descriptionHash);//执行提案交易

    return proposalId;
}
function _execute(
    uint256, /* proposalId */
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    bytes32 descriptionHash
) internal virtual override {
    _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash);//用timelock批量执行交易
}
function executeBatch(
    address[] calldata targets,
    uint256[] calldata values,
    bytes[] calldata datas,
    bytes32 predecessor,
    bytes32 salt
) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) {
    require(targets.length == values.length, "TimelockController: length mismatch");
    require(targets.length == datas.length, "TimelockController: length mismatch");

    bytes32 id = hashOperationBatch(targets, values, datas, predecessor, salt);
    _beforeCall(id, predecessor);
    for (uint256 i = 0; i < targets.length; ++i) {
        _call(id, i, targets[i], values[i], datas[i]);
    }
    _afterCall(id);
}
function _beforeCall(bytes32 id, bytes32 predecessor) private view {
    require(isOperationReady(id), "TimelockController: operation is not ready");//过了锁定期
    require(predecessor == bytes32(0) || isOperationDone(predecessor), "TimelockController: missing dependency");//依赖的交易必须是已完成,或者无依赖
}
function _call(
    bytes32 id,
    uint256 index,
    address target,
    uint256 value,
    bytes calldata data
) private {
    (bool success, ) = target.call{value: value}(data);//执行交易
    require(success, "TimelockController: underlying transaction reverted");//校验结果

    emit CallExecuted(id, index, target, value, data);//发出执行事件
}
function _afterCall(bytes32 id) private {
    require(isOperationReady(id), "TimelockController: operation is not ready");
    _timestamps[id] = _DONE_TIMESTAMP;//设置为已完成
}

实际的执行是调用TimelockController合约去执行的,合约地址是0xFe89cc7aBB2C4183683ab71653C4cdc9B02D44b7,这个合约继承了AccessControl做角色权限控制,以防止其它地址直接调用合约方法去执行交易。

一共有三个角色:

0x940ca438b31e8c4a6f192b1af78918299c4f37e395c68f7927be3a4c1f944242交易授予了ENS Governor合约提案角色,有这个角色才可以调用排队方法

而执行角色,是对外开放的。因为在执行时会先判断这个提案是否在排队中,而排队是有角色控制的,所以就不会出现没有排队被人恶意执行的情况。

Subscribe to franx.eth
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from franx.eth

Skeleton

Skeleton

Skeleton