DAO投票漏洞

  • mixbytes
  • 发布于 2024-07-28 15:18
  • 阅读 29

本文讨论了去中心化自治组织(DAO)中票选机制的技术漏洞,分析了不同DAO(如Aragon、Nexus Mutual等)在投票过程中遇到的具体安全风险,包括闪电贷攻击、重投漏洞、缺乏提案验证等。针对这些问题,作者依照实际案例提供了每种DAO的具体解决方案与保护措施,从而为改进现有的DAO投票机制提供了建议和参考。

作者:Konstantin Nekrasov - MixBytes 的安全研究员

代币投票

去中心化自治组织(DAO)在区块链上运行,并通过投票进行管理。代币投票是最受欢迎的方式:DAO的成员提出建议,其他代币持有者以代币投票表示同意。当提案达到法定人数时,可以执行其脚本。

这种方法存在合理的批评,维塔利克·布特林在以下链接中进行了强调 [1] [2] [3]

  1. 小规模富有参与者(“鲸鱼”)在成功执行决策方面优于大量小额持有者。
  2. 代币投票治理赋予代币持有者的利益,而牺牲了社区的其他部分。
  3. 对于同样持有与该平台互动的其他 DeFi 平台代币的代币持有者,可能会出现利益冲突。
  4. 代币投票对攻击者根本性的脆弱性:投票买卖 [4] [5] [7],投票借贷 [9] 和鲸鱼串通 [6]
  5. 暴露于复杂的博弈论攻击 [10]

这些都是非技术问题。此外,程序员可能因分心或对区块链工作原理的知识不足而在代码中引入纯技术漏洞。

有许多 DAO 使用代币投票:基于 Aragon 的 DAO、X-DAO、Nexus Mutual、Showball Finance、Pickle Finance、Spirit Swap、Keep3r Network 等等。

在本文中,我们将检查代币投票中可能出现的技术漏洞,并检查它们在上述一些 DAO 中是否存在。

攻击 #1. 闪电贷

如果黑客能够在同一区块内投票并执行提案(例如,通过紧急方法),DAO 可能存在漏洞。这类漏洞在 Beanstalk [5] 和 MakerDAO [11] 等项目中曾遇到过。

Aragon

Aragon 使用 MiniMeToken 的 balanceOfAt() 在提案创建的前一区块计算用户余额。这使得闪电贷攻击不可能。

X-DAO

X-DAO 在转移时会回滚 [→]

function transfer(address, uint256) public pure override returns (bool) {
    revert("GT: transfer is prohibited");
}

因此,不可能借出代币、在 DEX 上出售它,也不可能借入或购买,因此闪电贷攻击是不可能的。

Nexus Mutual

Nexus Mutual 的提案分为由顾问委员会投票的提案和由普通成员投票的提案。在顾问委员会投票中,每个参与者的权重相等于一。成员投票是普通代币投票。我们的重点仅在于代币投票。

如果所有其他成员都已经投票,则可以在一个事务中投票并执行提案 [→]

function canCloseProposal(uint _proposalId)
    ...
    if (numberOfMembers == proposalVoteTally[_proposalId].voters
      || dateUpdate.add(_closingTime) <= now)
      return 1;

但有一个复杂性——用户的代币在每次投票后在接下来的 7 天内不可转移 [→]

tokenInstance.lockForMemberVote(msg.sender, tokenHoldingTime);

因此,从技术上讲,黑客可以进行闪电贷,从市场上拉取 NXM 代币,在同一事务中投票并执行提案。但他们仍然需要偿还贷款,这需要从执行的提案中获得至少与闪电贷相同的利润。然而,由于在发现数据验证漏洞后引入的各种限制,似乎很难提出成功的攻击 [14]

Keep3r Network

该项目使用 类似 MiniMeToken 的代币。它在提案创建区块之前的一块计算投票权重通过 getPriorVotes()。可以得出相同的结论。

攻击 #2. 错误的重投

如果一个合同允许用户对提案进行重投,但以不正确的方式扣除用户的旧投票,则可能会出现漏洞。

建议检查以下危险场景:

  • 投票给不在提案列表中的提案;
  • 投票 → 转让 → 投票;
  • 在创建的同一块对提案投票;
  • 对同一提案 ID 使用错误的参数进行投票;
  • 重放离线交易。

这种类型的漏洞在 MakerDAO [12] 和 KP3R Network [13] 等项目中曾遭遇过。

Aragon

在 Aragon 中可以重投,但它正确地增加/减少上一次的投票权 [→]

// 这可能会重入,尽管我们可以假设治理代币不是恶意的。
uint256 voterStake = token.balanceOfAt(_voter, vote_.snapshotBlock);
VoterState state = vote_.voters[_voter];

// 如果选民之前投过票,则减少数量。
if (state == VoterState.Yea) {
    vote_.yea = vote_.yea.sub(voterStake);
} else if (state == VoterState.Nay) {
    vote_.nay = vote_.nay.sub(voterStake);
}

投票给不在提案列表中的提案

在投票方法中有一个 voteExists 修饰符,因此不可能对不存在的提案进行投票 [→]

function vote(
    uint256 _voteId,
    bool _supports,
    bool _executesIfDecided
) external voteExists(_voteId)

其他情况

用户的投票权不能转移并用于同一提案,因为 Aragon 在提案创建前的一块使用 MinimeToken 的余额。

X-DAO

X-DAO 的投票发生在链外,没有重新投票的机制。如果用户签署了拒绝,他们仍然可以签署批准,批准将被计算。但不能以相反的方式进行——只有正面的投票会被计算 [→]

for (uint256 i = 0; i < signers.length; i++) {
    share += balanceOf(signers[i]);
}

if (share * 100 < totalSupply() * quorum) {
    return false;
}

已签署的投票不能被计数两次 [→]

require(!_hasDuplicate(signers), "DAO: signatures are not unique.");

重放攻击

为了投票,用户签署一个提案,这只是一个数据集:目标地址、 calldata、msg 值、nonce、时间戳、block.chainid 和 X-DAO 实例地址。因此,在不同的以太坊链上、在另一个 X-DAO 实例上,甚至在具有不同 nonce 的另一个提案上重放用户签署的投票都是不可能的 [→]

function getTxHash
...
return
    keccak256(abi.encode(
        address(this),
        _target,
        _data,
        _value,
        _nonce,
        _timestamp,
        block.chainid
    ));
}

提案也无法重放 [→]

require(!executedTx[txHash], "DAO: voting already executed.");

Snowball Finance

用户不能投票给不在提案列表中的提案,但他们可以对相同的提案进行重投。合同在重投时正确地扣除了他们的先前决定。用户的代币被锁定在托管合约中,无法转移。

Spirit Swap

Spirit Swap 没有提案:用户只能选择并投票代币的权重。用户可以每周投票一次,而无法重投。

Keep3r Network

Keep3r Network 的行为类似于 Aragon,但用户不能对同一提案投票两次。

对不存在提案的投票在要求中失败 [→]

function _castVote...
    ...
    require(state(proposalId) == ProposalState.Active, "Governance::_castVote: voting is closed.");

同样,不可能重投 [→]

require(receipt.hasVoted == false, "Governance::_castVote: voter already voted.");

Nexus Mutual

不可能对不在提案列表中的提案进行投票,因为以下要求将失败 [→]

function submitVote(uint _proposalId, uint _solutionChosen) external {
    ...
    require(allProposalData[_proposalId].propStatus == uint(Governance.ProposalStatus.VotingStarted), "Not allowed");

用户只能对同一提案进行一次投票 [→]

function _submitVote(uint _proposalId, uint _solution) internal {
    ...
    require(memberProposalVote[msg.sender][_proposalId] == 0, "Not allowed");
    ...
    memberProposalVote[msg.sender][_proposalId] = totalVotes;

攻击 #3. 缺少提案验证

如果提案的属性没有完全验证,黑客可能会创造出看似良善的破坏性提案,存在社会工程的机会。

回答以下问题:

  1. 提案如何在网站上显示?
  2. 黑客能否在提案中提供任意脚本或 calldata?
  3. 黑客能否在恶意提案中提供任意良善描述?
  4. 普通用户判断提案的脚本实际做了什么是否困难?

在 Beanstalk [5] 和 Nexus Mutual [14] 等项目中曾遇到过验证不足的问题。

Aragon

在 Aragon 中,新的提案接受任意执行脚本和元数据 [→]

function newVote(bytes _executionScript, string _metadata) external auth(CREATE_VOTES_ROLE) returns (uint256 voteId)

元数据被发出,但没有保存在存储中,因此暗示后台将解析区块链中的事件,以显示提案作者地址和描述 [→]

emit StartVote(voteId, msg.sender, _metadata);

因此,可以提供任意执行脚本。因此,验证数据的责任落在投票者身上。

X-DAO

在 X-DAO 中没有链上的方法来创建提案。

可以在 xdao.app 上创建离线提案,并且有两点需要注意:

  1. 目标地址及其 calldata 默认是隐藏的,因此用户可能会误解恶意意图。提案示例见 链接
  2. 可以为提案指定任意标题和描述:链接

因此,验证数据的责任落在投票者身上。

Snowball Finance

在 Snowball Finance 中,新提案接收以下参数:

function propose(
    string calldata _title,
    string calldata _metadata,
    uint256 _votingPeriod,
    address _target,
    uint256 _value,
    bytes memory _data
)

并将所有这些参数存储到合同存储中,包括 msg.sender。

对 calldata 没有限制。所以,验证数据的责任落在投票者身上。

Spirit Swap

Spirit Swap 在投票中没有 ID 或元数据,因此此部分无关。

Keep3r Network

Keep3r Network 中的新提案接受一组目标、msg 值、calldata 和其他元数据 [→]

function propose(
    address[] memory targets,
    uint256[] memory values,
    string[] memory signatures,
    bytes[] memory calldatas,
    string memory description
)

攻击者仍然可以提供任意 calldata。因此,验证数据的责任落在投票者身上。

Nexus Mutual

Nexus Mutual 的提案分为不同类别。每个类别都有预定的操作地址和方法签名。网站上的提案清楚地显示了要调用的方法及其参数: https://app.nexusmutual.io/governance/view?proposalId=175

因此,似乎不存在社会工程机会。

攻击 #4. 无转移验证

代币锁定机制应检查批准的 transferFrom 调用(及其他类似的转移方法)的返回值:

  • Aragon Minime 代币在调用 transferFrom 函数失败时返回 false。

这种类型的漏洞曾在 ForceDAO 中遇到过 [15]

Nexus Mutual

Nexus Mutual 通过 tokenInstance.lockForMemberVote() 方法锁定代币,而不使用 transferFrom [→]

function lockForMemberVote(
    address _of,
    uint _days
) public onlyOperator {
    if (_days.add(now) > isLockedForMV[_of])
        isLockedForMV[_of] = _days.add(now);
}

Aragon、X-DAO 和 Keep3r Network:无效适用

Aragon、X-DAO 和 Keep3r Network 没有锁定机制。

Snowball Finance 和 Spirit Swap:托管

Snowball Finance 和 Spirit Swap 使用类似的托管合约来在一段时间内锁定代币。两个合约都检查转移的结果 [→]

assert ERC20(self.token).transferFrom(_addr, self, _value)
...
assert ERC20(self.token).transfer(msg.sender, value)

攻击 #5. 小投票窗口

对某些提案持消极态度的用户和否决权持有者可能没有时间作出反应。尤其是当法定人数少于 50% 时。

Aragon

投票和执行的开放时间为 voteTime [→]

function _isVoteOpen(Vote storage vote_) internal view returns (bool) {
    return getTimestamp64() < vote_.startDate.add(voteTime) && !vote_.executed;
}

这是一个全局参数,仅初始化一次。因此,社会工程的可能性取决于特定项目的初始化。

X-DAO

投票和执行的开放时间为 3 天 [→]

uint32 public constant VOTING_DURATION = 3 days;
...
require(
    _timestamp + VOTING_DURATION >= block.timestamp,
    "DAO: voting is over."
);

在我们看来,这应该足以让 DAO 的活跃参与者投票。

Snowball Finance

Snowball Finance 的时间周期是可变但有限的,可以由治理设置:

  • 投票期从 1 天到 30 天不等;
  • 执行延迟从 30 秒到 30 天不等;
  • 过期时间为 14 天。

在我们看来,这应该足以让 DAO 的活跃参与者投票。

Spirit Swap

Spirit Swap 每周允许投票一次 [→]

uint256 public voteDelay = 604800;

...

modifier hasVoted(address voter) {
    uint256 time = block.timestamp - lastVote[voter];
    require(time > voteDelay, "You voted in the last 7 days.");
    _;
}

在我们看来,这应该足以让 DAO 的活跃参与者投票。

Keep3r Network

Keep3r Network 的时间周期是可变但有限的,可以由治理设置:

  • 投票期从 1 天到 30 天不等;
  • 执行期为 14 天。

在我们看来,这应该足以让 DAO 的活跃参与者投票。

Nexus Mutual

提案可在 _closingTime 经过后关闭并执行 [→]

function canCloseProposal(uint _proposalId)
...
if (numberOfMembers == proposalVoteTally[_proposalId].voters
  || dateUpdate.add(_closingTime) <= now)
  return 1;

可以在网站上看到,参数从 3 到 7 天不等,具体取决于提案类别:

https://app.nexusmutual.io/governance/categories

在我们看来,这应该足以让 DAO 的活跃参与者投票。

攻击 #6. 双重投票

黑客是否可以对同一提案投票两次,使用相同的代币?

建议检查以下场景:

  • 投票 → 转让 → 再次投票;
  • 投票 → 委托 → 再次投票;
  • 调整 vote() 参数以增加额外的投票权;
  • 检查重入性。

Aragon

投票-转移-投票

可以在用户之间移动代币,但只在提案创建前的一块才重要,因此没有人能够双重投票。

投票-委托-投票

默认的 Aragon 合约中没有委托机制。

参数调整

没有参数可调整:

function vote(
    uint256 _voteId,
    bool _supports,
    bool _executesIfDecided
) external voteExists(_voteId)

重入性

投票可能会通过 _unsafeExecuteVote() 进行外部调用,但代码遵循检查-影响-交互模式,因此对重入性是免疫的。

X-DAO

投票-转移-投票

可以签署投票并将代币转移到另一个账户,以便他们可以签署另一个投票。但是 execute() 方法仅计数最终的代币分配,因此黑客的场景不适用。

其他情况

没有委托机制,也没有链上 vote() 方法,因此没有参数可以调整或尝试重新进入。

Spirit Swap

Spirit Swap 投票只会更改协议中的代币权重,并且在用户投票时应用更改。

投票-转移-投票

可以投票,等待托管合约中的代币解锁,将代币转移到另一个账户,再次锁定,然后为同一提案投票。但是计算表明,如果代币在整个过程中被简单地锁定,所应用的总投票权将是相同的。没有利益。

投票-委托-投票

用户在托管合约中锁定的投票权无法委托给其他用户。

参数调整

vote()方法接受代币和权重的数组,因此需要检查这些参数是否可以以某种方式被调整,以增加某些代币的额外投票权。如果你传递一个包含相同代币两次的数组会发生什么?

看起来投票方法会正确考虑其所有参数,并且在数组中传递相同的代币不会影响计算的正确性,因为所有权重都按总权重之和划分 [→]

for (uint256 i = 0; i < _tokenCnt; i++) {
    _totalVoteWeight = _totalVoteWeight + _weights[i];
}

重入性

接下来检查的是是否存在重入性。vote() 方法中存在外部调用 [→]

IBribe(bribes[gauges[_token]])._withdraw(...);
...
IBribe(bribes[_gauge])._deposit(uint256(_tokenWeight), _owner);

IBribe 合约可以视为可信,因此,即使这里可能存在漏洞,也不会构成威胁。

Snowball Finance

投票-转移-投票

可以投票,等待托管合约中的代币解锁,将代币转移到另一个账户,再次锁定,然后为同一提案投票。但是计算表明,如果代币在整个过程中被简单地锁定,所应用的总投票权将是相同的。没有利益。

投票-委托-投票

没有委托。

参数调整

没有参数可调整:

function vote(uint256 _proposalId, bool _support)

重入性

vote() 方法中没有不可信的外部调用。

Nexus Mutual

投票-转移-投票

这是不可能的,因为用户的转移在每次投票后被锁定 [→]

function _setVoteTally(uint _proposalId, uint _solution, uint mrSequence) internal {
    ...
    tokenInstance.lockForMemberVote(msg.sender, tokenHoldingTime);

投票-委托-投票

目前不允许委托 [→]

function delegateVote(address _add) external isMemberAndcheckPause checkPendingRewards {
    revert("Delegations are not allowed.");

但即使没有回滚,还有另一个要求,即如果用户刚刚进行了投票,他们不能委托其投票权 [→]

if (allVotesByMember[msg.sender].length > 0) {
    require((allVotes[allVotesByMember[msg.sender][allVotesByMember[msg.sender].length - 1]].dateAdd).add(tokenHoldingTime) < now);
}

参数调整

没有参数可调整:

function submitVote(uint _proposalId, uint _solutionChosen)

重入性

submitVote() 方法的代码遵循检查-影响-交互模式,因此没有重入性。

Keep3r Network

投票-转移-投票

Keep3r 的行为与 Aragon 类似,检查用户在提案创建前的一块的投票权。因此此情况不适用。

参数调整

没有参数可调整:

function castVote(uint256 proposalId, bool support)

重入性

castVote() 方法中没有不可信的外部调用。

攻击 #7. 双重执行

execute() 方法中是否存在重入性?可以在同一区块内调用两次吗?

Aragon、X-DAO、Nexus Mutual 和 Keep3r Network

Aragon、X-DAO 和 Keep3r Network 遵循检查-影响-交互模式,因此它们的执行方法不易受重入性攻击。

Snowball Finance

Snowball Finance 实现了 nonReentrant 修饰符,其执行方法也不易受重入性攻击。

Spirit Swap

Spirit Swap 没有执行() 方法:它在 vote() 方法中应用更改。

结论

这是在测试具有投票功能的 DAO 时值得检查的基本列表。欢迎使用!

链接

  1. 区块链治理笔记

    https://learnblockchain.cn/article/12449

  2. 在不知情的人中,区块链投票被高估,但在知情的人中被低估

    https://learnblockchain.cn/article/11648

  3. 超越代币投票治理

    https://learnblockchain.cn/article/11481

  4. 2022年10月6日,Mangata X 受到治理攻击,导致攻击者获得链上理事会的投票权

    https://blog.mangata.finance/blog/2022-10-08-council-incident-report/

  5. 2022年4月17日,犯罪者使用闪电贷利用 Beanstalk 的治理机制

    https://bean.money/blog/beanstalk-governance-exploit

  6. Tron 基金会 CEO Justin Sun 与大型加密交易所勾结,利用客户的代币为 Steem 网络投票,该网络的绝大多数社区成员强烈反对

    https://decrypt.co/38050/steem-steemit-tron-justin-sun-cryptocurrency-war

  7. 碳投票以太坊区块链投票实现的投票购买合同的概念证明

    https://gitlab.com/relyt29/votebuying-carbonvote

  8. 一个 uniswap 池,用于购买 $TRIBE——一个治理 Fei Protocol 的代币

    https://info.uniswap.org/#/tokens/0xc7283b66eb1eb5fb86327f08e1b5816b0720212b

  9. 一次加密投票的成本是多少?

    https://www.placeholder.vc/blog/2020/1/7/how-much-does-a-crypto-vote-cost

  10. 对 DAO 的暂时叫停的呼吁

    (发现了多种博弈论漏洞)

    https://hackingdistributed.com/2016/05/27/dao-call-for-moratorium/

  11. 在闪电贷被用来通过治理投票后,MakerDAO 发布了警告

    https://www.theblock.co/post/82721/makerdao-issues-warning-after-a-flash-loan-is-used-to-pass-a-governance-vote

  12. MakerDAO 治理中的关键漏洞技术描述

    https://blog.openzeppelin.com/makerdao-critical-vulnerability/

  13. KP3R 漏洞报告:Statemind 如何在 Keep3r Network 中发现两年前的漏洞

    https://statemind.io/blog/2022/09/27/gauge-proxy-bug.html

  14. Nexus Mutual——calldata 验证漏洞

    https://learnblockchain.cn/article/12424

  15. 2021年4月4日,ForceDAO DeFi 聚合器被一名白帽和四名黑帽黑客利用。恶意攻击者能够偷走 FORCE 代币。

    https://halborn.com/explained-the-forcedao-hack-april-2021/

  • MixBytes 是谁?

MixBytes 是一个专业的区块链审计和安全研究团队,专注于为兼容 EVM 和基于 Substrate 的项目提供全面的智能合约审计和技术咨询服务。请加入我们在 X 中,以获取最新的行业趋势和见解。

  • 原文链接: mixbytes.io/blog/dao-vot...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
mixbytes
mixbytes
Empowering Web3 businesses to build hack-resistant projects.