Platform: Code4rena
Start Date: 31/08/2023
Pot Size: $55,000 USDC
Total HM: 5
Participants: 30
Period: 6 days
Judge: hickuphh3
Total Solo HM: 2
Id: 282
League: ETH
Rank: 1/30
Findings: 3
Award: $16,701.04
π Selected for report: 1
π Solo Findings: 1
π Selected for report: Banditx0x
16539.426 USDC - $16,539.43
A delegate can subtract their own voting weight from the voting choice of another delegate, even if that user isn't a transcoder. Since they are not a transcoder, they don't have their votes initially increased by the amount delegated to them, voting weight is still subtracted from the tally of their vote choice.
Maliciously, this could be used to effectively double one's voting power, by delegating their votes to a delegator who is about to vote for the choice which they don't want. It can also occur accidentally, for example when somebody delegates to a transcoder who later switches role to delegate.
When a user is not a transcoder, their votes are determined by the amount they have delegated to the delegatedAddress, and does not increase when a user delegates to them:
if (bond.bondedAmount == 0) { amount = 0; } else if (isTranscoder) { amount = bond.delegatedAmount; } else { amount = delegatorCumulativeStakeAt(bond, _round); } }
Lets that this delegator (Alice) has 100 votes and votes For
, Then another delegator(Bob) has delegated 1000 votes to Alice As stated above, Alice doesn't get the voting power of Bob's 1000 votes, so the For
count increases by 100.
Bob now votes, and _handleVotesOverrides
is called. In this function, the first conditional, if isTranscoder
will return false as Bob is not self-delegating.
Then, there is a check that the address Bob has delegated to has voted. Note that there is a missing check of whether the delegate address is a transcoder. Therefore the logic inside if (delegateVoter.hasVoted)
is executed:
'''solidity if (delegateVoter.hasVoted) { // this is a delegator overriding its delegated transcoder vote, // we need to update the current totals to move the weight of // the delegator vote to the right outcome. VoteType delegateSupport = delegateVoter.support;
if (delegateSupport == VoteType.Against) { _tally.againstVotes -= _weight; } else if (delegateSupport == VoteType.For) { _tally.forVotes -= _weight; } else { assert(delegateSupport == VoteType.Abstain); _tally.abstainVotes -= _weight; } }
'''
The logic reduces the tally of whatever choice Alice voted for by Bob's weight. Alice initially voted For
with 100 votes, and then the For votes is reduced by Bob's 1000 votes
. Lets say that Bob votes Against
. This will result in an aggregate 900 vote reduction in the For
tally and +1000 votes for Agaisnt
after Alice and Bob has finished voting.
If Alice was a transcoder, Bob will be simply reversing the votes they had delegated to Alice. However since Alice was a delegate, they never gained the voting power that was delegated to her.
Bob has effectively gained the ability to vote against somebody else's votes (without first actually increasing their voting power since they are not a transcoder) and can vote themselves, which allows them to manipulate governance.
Manual Review
There should be a check that a delegate is a transcoder before subtracting the tally. Here is some pseudocode:
if (delegateVoter.hasVoted && ---delegate is transcoder ---)
This is an edit of the conditional of the function _handleOverrides
. This ensures that the subtraction of vote tally is only performed when the delegate is a voter AND the delegate is a transcoder. This should fix the accounting/subtraction issue of vote tally for non-transcoder delegates.
Invalid Validation
#0 - c4-pre-sort
2023-09-09T13:27:55Z
141345 marked the issue as sufficient quality report
#1 - c4-sponsor
2023-09-14T20:38:36Z
victorges (sponsor) confirmed
#2 - c4-judge
2023-09-18T03:24:59Z
HickupHH3 marked the issue as selected for report
#3 - c4-judge
2023-09-18T03:25:04Z
HickupHH3 marked the issue as satisfactory
π Selected for report: Proxy
Also found by: Banditx0x, DavidGiladi, favelanky, ladboy233, nadin, rvierdiiev
62.8682 USDC - $62.87
Claim Rounds claims pool token shares for the whole round rather than until the end round that the function caller specifies.
The claimEarnings
function takes in an _endRound parameter. However, this parameter does not affect the round until which the rewards are claimed. Instead, rewards are claimed until the current round.
function claimEarnings(uint256 _endRound) external whenSystemNotPaused currentRoundInitialized autoCheckpoint(msg.sender) { _endRound; _autoClaimEarnings(msg.sender); }
This does not match the code comments which clearly state that the tokens should be claimed until the end round:
@notice Claim token pools shares for a delegator from its lastClaimRound through the end round
Manual Review
Use the _endRound
parameter within the claimEarnings
function and set it as the upper bound until which rewards are claimed.
Other
#0 - 141345
2023-09-08T15:51:51Z
not comply with comment spec
lack impact/loss
QA is more appropriate.
#1 - c4-pre-sort
2023-09-09T14:50:23Z
141345 marked the issue as sufficient quality report
#2 - victorges
2023-09-15T17:45:24Z
This has been an explicit decision to simplify the implementation while keeping the future-proof interface. The parameter is ignored explicitly with a dummy usage for silencing any warnings. I believe the documentation could be improved to reflect this current state, but definitely don't think this is a med risk issue in the code.
Acknowledging, but should be at most QA.
#3 - c4-sponsor
2023-09-15T17:45:30Z
victorges (sponsor) acknowledged
#4 - c4-sponsor
2023-09-15T17:45:34Z
victorges marked the issue as disagree with severity
#5 - HickupHH3
2023-09-18T10:44:45Z
QA(L).
#6 - c4-judge
2023-09-18T10:44:53Z
HickupHH3 changed the severity to QA (Quality Assurance)
#7 - c4-sponsor
2023-09-20T23:54:09Z
victorges (sponsor) confirmed
π Selected for report: catellatech
Also found by: 0x3b, ADM, Banditx0x, JayShreeRAM, Krace, Sathish9098
My audit approach focused on:
BondingManager.sol
I paid special attention to the code which was updated in the latest pull request, such as the BondingVotes
contract.
I found vulnerabilities in all the above categories.
One mental mode I used for the code was based the "Assets, Actors and Actions" model from Secureum. Below are some notes I collected on Actors and the Actions they can take. Actors are in bold and actions are in dot points in the codebase Analysis:
Transcoders:
Rounds Manager:
Verifier:
Delegator:
Any User:
The relevant assets in this audit are votes and tokens. Although votes are not technically an asset, incorrect flow of assets can result in security vulnerabilities.
The logic for checkpointing is in the BondingVotes.sol
contract. These checkpoints are then collected in the functions in BondingManager.sol
, mainly using the modifier autoCheckpoint
. This modifier runs after the function logic has executed, which makes sure that the checkpoint records the state after all state changes have occurred.
I spent around 40 hours on this audit
40 hours
#0 - c4-judge
2023-09-21T15:15:39Z
HickupHH3 marked the issue as grade-b