code-423n4 / 2023-08-arbitrum-findings

3 stars 3 forks source link

Decline in voting weight over time can be circumvented by splitting votes over multiple accounts and voting with the correct amount of votes #241

Open code423n4 opened 1 year ago

code423n4 commented 1 year ago

Lines of code

https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L252-L253

Vulnerability details

Impact

Decline in voting weight over time can be circumvented by splitting votes over multiple accounts and voting with the correct amount of votes

Proof of Concept

According to the Arbitrum documentation (https://forum.arbitrum.foundation/t/proposal-security-council-elections-proposed-implementation-spec/15425)

"The voting process is designed to encourage voters to cast their vote early. Their voting power will eventually decay if they do not cast their vote within the first 7 days:

0 - 7 days. Votes cast will carry weight 1 per token 7 - 21 days. Votes cast will have their weight linearly decreased based on the amount of time that has passed since the 7 day point. By the 21st day, each token will carry a weight of 0."

The implementation of the "votesToWeight" function in SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol implements the time-based decrease of votes and discounts this from the votes given: https://github.com/ArbitrumFoundation/governance/blob/c18de53820c505fc459f766c1b224810eaeaabc5/src/security-council-mgmt/governors/modules/SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L252-L255

But because of the reason that Solidity rounds fractional numbers down to the next lower integer the calculated decrease can round down to 0. See the following examples:

The blocks are the Ethereum blocks since block.number in Arbitrum is the synced Ethereum L1 block number. Ethereum has 7200 blocks/day (5 blocks/min 60 min/hour 24h/day = 7200 blocks/day

Part 1 of the voting period: 0-7 days (7 days) / full weight = 50400 blocks Part 2 of the voting period: 7-21 days (14 days) / linearly declining weight = 100800 blocks

start block = 20000000

end block = startblock + 7 days + 14 days end block = 20000000 + 50400 + 100800 = 20151200

full weight voting deadline = start block + 7 days full weight voting deadline = 20000000 + 50400 = 20050400

decreasing weight duration = end Block - full weight voting deadline decreasing weight duration = 20151200 - 20050400 = 100800

Decrease amount = votes * (block number - full weight voting deadline) / decreasing weight duration

1) Example at 25% of decreasing weight timeframe block = 20050400 + 0.25 (20151200 - 20050400) = 20075600 decrease amount = 3 (20075600 - 20050400) / 100800 = 0.75 -> rounds down to 0 Result: Voting at this block with 3 votes will not reduce the weight of the vote.

2) Example at 50% of decreasing weight timeframe block = 20050400 + 0.5 (20151200 - 20050400) = 20100800 decrease amount = 1 (20100800 - 20050400) / 100800 = 0.5 -> rounds down to 0 Result: Voting at this block with 1 vote will not reduce the weight of the vote.

3) Example at 75% of decreasing weight timeframe block = 20050400 + 0.75 (20151200 - 20050400) = 20126000 decrease amount = 1 (20126000 - 20050400) / 100800 = 0.75 -> rounds down to 0 Result: Voting at this block with 1 vote will not reduce the weight of the vote.

4) Example at 85% of decreasing weight timeframe block = 20050400 + 0.85 (20151200 - 20050400) = 20136080 decrease amount = 1 (20136080 - 20050400) / 100800 = 0.85 -> rounds down to 0 Result: Voting at this block with 1 vote will not reduce the weight of the vote.

4) Example at 99% of decreasing weight timeframe block = 20050400 + 0.99 (20151200 - 20050400) = 20150192 decrease amount = 1 (20150192 - 20050400) / 100800 = 0.99 -> rounds down to 0 Result: Voting at this block with 1 vote will not reduce the weight of the vote.

The examples above show that the time-based decrease in voting power can be circumvented with the right amount of votes chosen at the right amount in time. This allows an attacker to influence an election even close to the end of the voting period if he split his votes over many accounts. The decreased amount of cost/transaction on Arbitrum compared to Ethereum main net facilitates such an attack.

Tools Used

Manual review

Recommended Mitigation Steps

A minimum amount of votes could be used to prevent rounding down to a 0 time-based decrease in voting power.

Assessed type

Math

c4-pre-sort commented 1 year ago

0xSorryNotSorry marked the issue as primary issue

c4-sponsor commented 1 year ago

yahgwai marked the issue as sponsor acknowledged

c4-sponsor commented 1 year ago

yahgwai marked the issue as disagree with severity

yahgwai commented 1 year ago

Severity: Med or Low, as not possible to exploit. The maximum amount of increased weight that a user can gain is 1. Therefore they would need to heavily split their vote (1e18 times) in order to cause 1 ARB votes worth of difference. This makes the attack ineffective.

0xean commented 1 year ago

This is essentially a dust attack, and maybe could qualify as a "leak of value" type issue making me think M is the correct severity, although when one consider the likelihood I think QA / Low may be more appropiate.

Will downgrade to M for now and welcome more conversation on the topic during the QA process.

c4-judge commented 1 year ago

0xean changed the severity to 2 (Med Risk)

c4-judge commented 1 year ago

0xean marked the issue as satisfactory

c4-judge commented 1 year ago

0xean marked the issue as selected for report

MiloTruck commented 1 year ago

Hi @0xean, I don't think this issue should be a medium severity finding, as it isn't even remotely close to exploitable at all.

The example above states that 1 vote corresponds to 1, which is incorrect as it doesn't take decimals into account. 1 ARB token corresponds to 1 vote, and since ARB token has 18 decimals, 1 vote would actually be 1e18.

This is further mitigated by the decreaseAmount formula as its numerator actually increases when votes should have less weightage:

SecurityCouncilMemberElectionGovernorCountingUpgradeable.sol#L252-L255

        uint256 decreaseAmount =
            votes * (blockNumber - fullWeightVotingDeadline_) / decreasingWeightDuration;
        // subtract the decreased amount to get the remaining weight
        return _downCast(votes - decreaseAmount);

As blockNumber - fullWeightVotingDeadline_ is only a few decimals less than decreasingWeightDuration, for an attacker to leverage rounding to gain a significant amount of votes, he would have to send an insane amount of transactions.

For example, at 99% of decreasing weight timeframe, for an attacker to have full weightage for 1 vote, he would have to send 10**18 transactions, which is extremely impractical. At this point, it is more logical for the attacker to just buy more ARB tokens with the amount he paid in transaction fees...

Would appreciate it if you could take a second look at this finding, thanks!

0xean commented 1 year ago

@MiloTruck - thanks for the response and agree that QA is probably the right outcome for this issue.

c4-judge commented 1 year ago

0xean changed the severity to QA (Quality Assurance)

c4-judge commented 1 year ago

0xean marked the issue as grade-a

c4-judge commented 1 year ago

0xean marked the issue as not selected for report