Dao投票相关漏洞以及解决方案

Posted by Thomas_Xu on 2023-01-05

Dao投票相关漏洞以及解决方案


author:Thomas_Xu

引言

去中心化自治组织(DAO)在区块链中运作,由投票管理。 Coin voting是最受欢迎的一种:DAO 的成员提出建议,其他token持有者用代币表示认可。当达到提案的法定人数时,可以执行其脚本。

有许多DAO使用Coin voting:基于Aragon的DAO,X-DAONexus MututalShowball FinancePickle FinanceSpirit SwapKeep3r Network等等。

在本文中,我们将研究Coin voting中可能出现的技术漏洞,并检查它们是否存在于上述这些 DAO 中。

闪贷攻击

如果黑客可以在同一区块中投票并执行提案(例如通过紧急方法),则 DAO 可能容易受到攻击。这些漏洞以前在BeanstalkMakerDAO等项目中遇到过。

Aragon

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 查询特定_blockNumber处_owner的余额
function balanceOfAt(address _owner, uint _blockNumber) public constant returns (uint) {
if ((balances[_owner].length == 0) || (balances[_owner][0].fromBlock > _blockNumber)) {
if (address(parentToken) != 0) {
return parentToken.balanceOfAt(_owner, min(_blockNumber, parentSnapShotBlock));
} else {
// Has no parent
return 0;
}

// 这将在正常情况下返回预期余额
} else {
return getValueAt(balances[_owner], _blockNumber);
}
}

X-DAO

X-DAO 在转账时时恢revert:

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

因此,不可能借出代币或在DEX中出售代币以及借入或购买,因此闪电贷款攻击是不可能的。

Nexus Mutual

Nexus Mutual中的提案分为只有顾问委员会才能投票的提案和普通成员可以投票的提案。在咨询委员会投票中,每个参与者的权重等于一个。成员投票是普通的Coin voting。我们的范围只是Coin voting

如果所有其他成员都已投票[→],则可以在一次交易中投票并执行提案:

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

但有一个复杂的问题——用户转移代币的能力在每次投票后的 7 天内被锁定 [→]

1
tokenInstance.lockForMemberVote(msg.sender, tokenHoldingTime);

因此,从技术上讲,黑客可以进行闪电贷款,从市场中提取NXM代币,在同一笔交易中投票和执行提案。但他们仍然需要归还贷款,这需要从执行的提案中获得不低于闪电贷款本身的利润。但是,由于发现calldata验证漏洞后引入的各种限制,似乎很难提出成功的攻击。

Keep3r Network

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

不正确的重新投票

如果合同允许用户对提案重新投票,但它错误地减去了用户的旧投票,则可能会出现漏洞。

需要检查以下危险情况:

  • 投票支持不存在的提案;
  • 投票→转让→投票;
  • 在创建提案的同一区块中投票支持提案;
  • 使用损坏的参数投票,但使用相同的提案 ID;
  • 重播链下交易。

这些类型的漏洞以前在MakerDAO和KP3R Network等项目中遇到过。

Aragon

可以在Aragon重新投票,但它可以正确添加/减去以前的投票权[→]

1
2
3
4
5
6
7
8
9
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 修饰符,因此无法投票给不存在的提案 [→]

1
2
3
4
5
function vote(
uint256 _voteId,
bool _supports,
bool _executesIfDecided
) external voteExists(_voteId)

其他可能情况

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

X-DAO

X-DAO中的投票发生在链下,没有对同一提案重新投票的机制。如果用户签署了拒绝,他们仍然可以发出已签名的approve,并且approve将被计算在内。但不能反过来做——只计算赞成票[→]

1
2
3
4
5
6
7
for (uint256 i = 0; i < signers.length; i++) {
share += balanceOf(signers[i]);
}

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

签名的投票不能计算两次[→]

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

重放攻击

要投票,用户签署一个提案,该提案只是一组数据:目标地址,调用数据,msg值,nonce,时间戳,block.chainid和X-DAO实例的地址。因此,既不可能在不同的以太坊链上重播用户的签名投票,也不可能在另一个 X-DAO 实例上重播,甚至不可能在另一个具有不同随机数 [→] 的提案上重播:

1
2
3
4
5
6
7
8
9
10
11
12
13
function getTxHash
...
return
keccak256(abi.encode(
address(this),
_target,
_data,
_value,
_nonce,
_timestamp,
block.chainid
));
}

提案也不能重播 [→]

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

Snowball Finance

用户不能为不存在的提案投票,但可以对同一提案重新投票。合同正确地减去了他们之前关于重新投票的决定。用户代币被锁定在托管合约中,无法转移。

Spirit Swap

用户只能选择和投票代币权重。用户可以每周投票一次,但无法重新投票。

Keep3r Network

Keep3r网络的行为与Aragon类似,但用户不能对同一提案进行两次投票。

对不存在的提案进行投票不符合要求 [→]

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

也不可能重新投票[→]

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

Nexus Mutual

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

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

用户只能为提案投票一次 [→]

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

缺少提案验证

如果提案属性未经过完全验证,那么黑客可能会有社会工程机会创建看起来良性的破坏性提案。

解决以下问题:

  1. 提案如何在网站上显示?
  2. 黑客可以在提案中提供任意脚本或调用数据吗?
  3. 黑客能否在恶意提案中提供任意良性描述?
  4. 普通用户很难确定提案脚本的真正作用吗?

BeanstalkNexus Mutual等项目中,已经遇到过弱验证。

Aragon

Aragon的新提案采用任意执行脚本和元数据 [→]

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

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

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

因此,可以提供任意执行脚本。因此,核实数据的责任落在了选民身上。

X-DAO

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

可以在 xdao.app 上创建链下提案,有两点需要注意:

  1. 默认情况下,目标地址及其呼叫数据处于隐藏状态,因此用户可能会错过恶意意图。请参阅链接中的提案示例
  2. 可以为提案指定任意提案标题和描述:链接

因此,核实数据的责任落在了选民身上。

Snowball Finance

Snowball Finance 的一份新提案提出了以下参数:

1
2
3
4
5
6
7
8
function propose(
string calldata _title,
string calldata _metadata,
uint256 _votingPeriod,
address _target,
uint256 _value,
bytes memory _data
)

并将所有这些存储在合约的存储中,包括msg.sender。

对调用数据没有限制。因此,核实数据的责任落在了选民身上。

Keep3r Network

Keep3r Network 中的新提案需要目标、msg 值、calldata 和其他元数据的列表 [→]

1
2
3
4
5
6
7
function propose(
address[] memory targets,
uint256[] memory values,
string[] memory signatures,
bytes[] memory calldatas,
string memory description
)

攻击者仍可以提供任意调用数据。因此,核实数据的责任落在了选民身上。

无转移验证

令牌锁定机制应检查已批准的 transferFrom 调用(以及其他类似传输的方法)的返回值:

  • 如果对 transferFrom 函数的调用失败,则 Aragon Minime 令牌返回 false。

这种类型的漏洞以前在ForceDAO中遇到过。

Nexus Mutual

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

1
2
3
4
5
6
7
function lockForMemberVote(
address _of,
uint _days
) public onlyOperator {
if (_days.add(now) > isLockedForMV[_of])
isLockedForMV[_of] = _days.add(now);
}

Snowball Finance和Spirit Swap

Snowball Finance和Spirit Swap正在使用类似的托管合约来锁定代币一段时间。两种检查传输都会导致 [→]

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

Aragon,X-DAO和Keep3r网络均没有锁定机制。

投票窗口小

投票窗口过小,会导致对某些提案持消极倾向的用户和否决权持有者可能没有时间作出反应。特别是当法定人数小于 50% 时。

Aragon

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

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

这是一个全局参数,初始化一次。因此,投票窗口取决于特定项目的初始化。

X-DAO

投票和执行的有效期为3天[→]

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

X-DAO认为,三天应该足以让 DAO 的积极参与者投票。

Snowball Finance

Snowball Finance 具有可变但有限的时间段,可由治理机构设置:

  • 投票期从1天到30天不等;
  • 执行延迟从 30 秒到 30 天不等;
  • 有效期为 14 天在Snowball Finance看来,这应该足以让 DAO 的积极参与者投票。

Spirit Swap

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

1
2
3
4
5
6
7
8
9
uint256 public voteDelay = 604800;

...

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

Spirit Swap看来,这应该足以让 DAO 的积极参与者投票。

Nexus Mutual

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

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

在网站上可以看到,根据提案类别的不同,此参数从 3 天到 7 天不等:
https://app.nexusmutual.io/governance/categories

这应该足以让 DAO 的积极参与者投票。

双重投票

黑客可以对具有相同令牌的提案进行两次投票吗?

建议检查:

  • 再次投票→转移→投票;
  • 再次投票→代表→投票;
  • 修改 vote() 参数以增加额外的投票权;
  • 检查重入。

Aragon

  • 投票-转移-投票

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

  • 投票-代表-投票

默认的阿拉贡合同中没有授权机制。

  • 修改参数

没什么可破坏的:

1
2
3
4
5
function vote(
uint256 _voteId,
bool _supports,
bool _executesIfDecided
) external voteExists(_voteId)
  • 重入

投票可以通过 _unsafeExecuteVote() 导致外部调用,但代码遵循检查效果交互模式,因此不受重入的影响。

X-DAO

  • 投票-转移-投票

可以签署投票并将代币转移到另一个帐户,以便他们也可以签署另一次投票。但是execute()方法只计算最终的令牌分发,因此黑客场景不适用。

  • 其他案例

没有委托机制,也没有链上 vote() 方法,因此没有什么可以破坏或尝试重新进入的。

Spirit Swap

Spirit Swap投票只能更改协议中的代币权重,并且它以与用户投票相同的方法应用更改。

  • 投票-转移-投票

可以投票,等到代币在托管合约中解锁,将代币转移到另一个账户,再次锁定它们,然后投票支持同一个提案。但计算表明,如果代币在整个时间内被锁定,应用的总投票权将是相同的。没有好处。

  • 投票-代表-投票

锁定在托管合同中的用户的投票权不能委托给其他用户。

  • 修改参数

vote() 方法采用一个令牌和权重数组,因此检查这些参数是否可以以某种方式修改以增加对某些令牌的额外投票权非常重要。如果两次传递具有相同令牌的数组会发生什么?

投票方法似乎正确考虑了其所有参数,并且在数组中传递相同的标记不会影响计算的正确性,因为所有权重都除以数组中传递的总权重总和 [→]

1
2
3
for (uint256 i = 0; i < _tokenCnt; i++) {
_totalVoteWeight = _totalVoteWeight + _weights[i];
}
  • 重入

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

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

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

Snowball Finance

  • 投票-转移-投票

可以投票,等到代币在托管合约中解锁,将代币转移到另一个账户,再次锁定它们,然后投票支持相同的提案。但计算表明,如果代币在整个时间内被锁定,应用的总投票权将是相同的。没有好处。

  • 投票-代表-投票

没有授权。

  • 修改参数

没什么可破坏的:

1
function vote(uint256 _proposalId, bool _support)
  • 重入

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

Nexus Mutual

  • 投票-转移-投票

这是不可能的,因为用户的传输在每次投票后都会被锁定 [→]

1
2
3
function _setVoteTally(uint _proposalId, uint _solution, uint mrSequence) internal {
...
tokenInstance.lockForMemberVote(msg.sender, tokenHoldingTime);
  • 投票-代表-投票

目前不允许委派 [→]

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

但即使没有还原,还有另一个要求,即如果用户最近投票[→],则不能委托其投票权:

1
2
3
if (allVotesByMember[msg.sender].length > 0) {    
require((allVotes[allVotesByMember[msg.sender][allVotesByMember[msg.sender].length - 1]].dateAdd).add(tokenHoldingTime) < now);
}
  • 修改参数

没什么可破坏的:

1
function submitVote(uint _proposalId, uint _solutionChosen)
  • 重入

submitVote() 方法代码遵循检查效果交互模式,因此没有重入。

Keep3r Network

  • 投票-转移-投票

Keep3r具有类似于Aragon的行为,并在创建提案之前检查用户在一个区块的投票权。因此,本案不适用。

  • 修改参数

没什么可破坏的:

1
function castVote(uint256 proposalId, bool support)
  • 重入

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

双重执行

execute() 方法中是否有重入?它可以在同一个块中调用两次吗?

Aragon,X-DAO,Nexus Mutual和Keep3r网络

Aragon,X-DAO和Keep3r网络使用检查效果交互模式,因此它们的执行方法不易受到重入的影响。

Snowball Finance

Snowball Finance 实现了非重入修饰符,其执行方法也不容易受到重入的影响。

Spirit swap

Spirit swap 没有 execution() 方法:它应用 vote() 方法中的更改。

参考链接

  1. 区块链治理
    https://vitalik.ca/general/2017/12/17/voting.html 注意事项
  2. 区块链投票在不知情的人中被高估,但在知情人士
    中被低估 https://vitalik.ca/general/2021/05/25/voting2.html
  3. 超越Coin voting治理
    https://vitalik.ca/general/2021/08/16/voting3.html
  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. 波场基金会首席执行官贾斯汀·孙(Justin Sun)与大型加密货币交易所勾结,并利用其客户的Coin Voting支持收购Steem网络,该网络遭到社区大多数人的强烈反对
    https://decrypt.co/38050/steem-steemit-tron-justin-sun-cryptocurrency-war
  7. CarbonVote以太坊区块链投票实施
    https://gitlab.com/relyt29/votebuying-carbonvote 的概念验证投票购买合约
  8. 一个用于购买$TRIBE的单交换池 — 一种管理 Fei 协议
    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 网络
    https://statemind.io/blog/2022/09/27/gauge-proxy-bug.html 中发现一个两年前的漏洞
  14. Nexus Mutual – 呼叫数据验证错误
    https://medium.com/nexus-mutual/responsible-vulnerability-disclosure-ece3fe3bcefa
  15. 2021 年 4 月 4 日,ForceDAO DeFi 聚合器被一名白帽黑客和四名黑帽黑客利用。恶意攻击者能够窃取 FORCE 代币。
    https://halborn.com/explained-the-forcedao-hack-april-2021/

notice

true

This is copyright.

...

...

00:00
00:00