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: 7/30
Findings: 1
Award: $2,232.82
π Selected for report: 0
π Solo Findings: 0
2232.8225 USDC - $2,232.82
Slashed transcoders can still become active transcorders by bonding an amount again to increase the total stake, which can inflate the actual delegatedAmount
, giving those transcorders more power voting power than it should.
Every time someone bonds or unbonds tokens in the system, it will modify different storage values, by passing trough two important functions, increaseTotalStake
and decreaseTotalStake
. Each of these functions modify the delegatedAmount
by adding or subtracting the specific amount a delegator or a transcoder bounded or unbounded from the protocol
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L1344
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L1383
Delegated amount is an important variable that can be used later, after the checkpoints in BondingVotes.sol
trough _checkpointBondingState
function, by transcoders to weight their voting power in LivepeerGovernor.sol
. The problem relies in the case of a slashed transcoder, since the delegated amount that he has is not updated in every case, which can mess up calculations in the long run. As you can see slashTranscoder
can be called to slash a specific bonded amount of a transcoder and resign him from the transcoderPool
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L394-L408
amount which is calculated as a percentage of the total amount bonded and then subtracted from his total amount bonded
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L403
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L411
In the next if statement , if there is still a bonded amount, then it will subtract the penalty also from his delegatedAmount
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L414-L417
and then burns/transfers the penalty and fees to the finder, if that is the case.
The problem relies in the case where a transcoder gets all of his bonded amount slashed, because in that case the if statement here
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L414
will not pass since the delegator status will be unbounded
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L959-L961
which will not make any subtraction from the total delegatedAmount
, leaving the slashed amount of the transcoder still in the delegatedAmount
since it doesn't get subtracted anywhere else. Now even if the transcoder gets resign here
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L407
he can still bond any amount which will increaseTotalStake
and will check if isRegisteredTranscoder
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L1318
which will be the case here since isRegisteredTranscoder
checks for these 2 conditions
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L1158
which will both be true, since the delegateAddress
is not getting deleted anywhere in the slashTranscoder
or any other modifier, and bondedAmount
will be greater than 0 since the bonding happens before increasing the total stake amount. Because of that the slashed transcoder will tryToJoinActiveSet
and he can become a transcoder again. When the delegatedAmount
gets updated after
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L1344
all of his slashed amount will still be existing into the variable, even though the amount got burn, and the new amount will be added, which will inflate his actual delegatedAmount
, since his first share should not be there anymore because it got burn. That will checkpoint the transcoder with and inflated amount, which will give him more voting power in LivepeerGovernor.sol
than he should have.
Manual review
Consider subtracting the amount burned from the delegatedAmount
all the time, since any amount he bond/undbond will increase/decrease the delegatedAmount
every time beside this special case where the actual value of delegatedAmount
can get inflated.
Other
#0 - c4-pre-sort
2023-09-09T14:46:59Z
141345 marked the issue as sufficient quality report
#1 - victorges
2023-09-15T17:18:27Z
Even though we don't really use slashTranscoder
today, the report is valid given the invalid state that the transcoder would be into. Not sure if we're gonna worry fixing it, but acknowledging as an issue to the least
#2 - c4-sponsor
2023-09-15T17:18:30Z
victorges (sponsor) acknowledged
#3 - HickupHH3
2023-09-18T10:40:20Z
i'm grouping this together with #194 because it is a case of not handling fully slashed transcoders correctly - and that is intended because slashing has been deprecated.
#4 - c4-judge
2023-09-18T10:40:50Z
HickupHH3 marked the issue as duplicate of #194
#5 - c4-judge
2023-09-18T10:41:12Z
HickupHH3 marked the issue as satisfactory
2232.8225 USDC - $2,232.82
If a transcoder gets slashed fully he can still vote with 0 amount of weight
making any other delegated user that wants to change his vote to subtract their weight
amount from other delegators/transcoders.
In BondingManager.sol
any transcoder can gets slashed by a specific percentage, and that specific transcoder gets resigned and that specific percentage gets deducted from his bondedAmount
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L394-L411
If any bondedAmount
will remain then the penalty will also gets subtracted from the delegatedAmount
, if not, nothing happens
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L412-L417
After that the penalty
gets burned and the fees gets paid to the finder, if that is the case
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L420-L440
The problem relies in the fact that a fully slashed transcoder, even if it gets resigned, he is still seen as an active transcoder in the case of voting. Let's assume this case :
bondedAmount
to 0, but he still has delegatedAmount
to his address since nothing happens this variable
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingManager.sol#L402-L418_getVotes
from GovernorVotesUpgradeable.sol
https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/f34a3a7e5a1d698d87d517fda698d48286310bee/contracts/governance/extensions/GovernorVotesUpgradeable.sol#L55-L61_getVotes
calls getPastVotes
on BondingVotes.sol
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingVotes.sol#L167-L170 which returns the amount of weight specific to this transcoder and as you can see, because the transcoder has a bondedAmount
equal to 0, the first if statement will be true and 0 will be returned https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingVotes.sol#L372-L373_countVote
which will then be passed into _handleVoteOverrides
https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L151slashTranscoder
function, or any other function the transcoder delegateAddress
gets changed, so this if statement will be true https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L182-L184, which will try to deduct the weight from any previous delegators https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L184-L189for
, against
, abstain
, but since 0 weight is passed no changed will be made.delegatorCumulativeStakeAt
gets called https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/bonding/BondingVotes.sol#L459-L487 which will return most of the time his bondedAmount
, amount which is greater than 0, since he didn't unbound._handleVoteOverrides
gets called in _countVote
, to override the vote, this if statement will be true, since the transcoder voted already, but with 0 weight https://github.com/code-423n4/2023-08-livepeer/blob/a3d801fa4690119b6f96aeb5508e58d752bda5bc/contracts/treasury/GovernorCountingOverridable.sol#L195 and the delegator weight gets subtracted from the support that the transcoder used in his votedelegatedAmount
, which will make the subtraction viable, since the weight of the delegator should be included in the full delegatedAmount
of that specific transcoder, but since the transcoder voted with 0 weight, the subtraction would be done from other delegators/transcoders votes.abstain
and every other voter will vote on for
or against
, every time one of his delegators want to change the vote the subtraction can revert, which will force those delegators out of the vote, until they will change their transcoderManual review
If a transcoder gets fully slashed and resigned, delete his address from delegateAddress
so he will not appear as a transcoder in the mechanism of counting the votes. If he still wants to participate in the system he can act as a delegator to another transcoder. Another solution would be to not let 0 weight votes happen anyways, since they don't modify the vote state at all.
Governance
#0 - c4-pre-sort
2023-09-09T14:49:13Z
141345 marked the issue as sufficient quality report
#1 - c4-sponsor
2023-09-14T20:44:51Z
victorges marked the issue as disagree with severity
#2 - victorges
2023-09-14T20:49:06Z
Slashing is turned off in the protocol, so this specific issue is not that important. It did point towards an issue in BondingVotes
tho in the way that the getBondingState
function checks if transcoder is a transcoder or not and defaults to 0 when bondedAmount
is 0
. There are other ways that this logic can be triggered (not through slashTranscoder
) that can show inconsistencies as well, so I'm flagging this as valid for the underlying issue that it hints to, even though it does not describe a realistic scenario about how this issue could happen.
#3 - c4-sponsor
2023-09-14T20:49:11Z
victorges (sponsor) confirmed
#4 - HickupHH3
2023-09-18T04:20:23Z
I found the POC hard to follow. A coded instance would have tremendously helped, but what I gather is:
_handleVoteOverrides()
uses the slashed transcoder's bondedAmount
instead of the 0 weight.
so this issue seems valid, although its premise is that the slashing functionality is in place.The README did not indicate that slashing was turned off, though I see it's mentioned a couple of times in the Discord channel. Should've been added in the "Known caveats / limitations" section.
Hence, IMO it isn't entirely fair to wardens if I invalidate their issue because they weren't aware of this fact.
Leaning towards a downgrade to M because of the external requirement of the active slashing mechanism, but am open to discussion.
#5 - c4-judge
2023-09-18T10:38:52Z
HickupHH3 changed the severity to 2 (Med Risk)
#6 - c4-judge
2023-09-18T10:40:38Z
HickupHH3 marked the issue as primary issue
#7 - c4-judge
2023-09-18T10:41:00Z
HickupHH3 marked the issue as selected for report
#8 - c4-judge
2023-09-18T10:41:04Z
HickupHH3 marked the issue as satisfactory
#9 - HickupHH3
2023-09-24T09:30:14Z
Revisiting this issue, IMO it's unreasonable to expect the sponsor to explicitly list out all the gotchas / limitations of the protocol. Some will only pop into mind through discussions with wardens, which seemed to be the case here when the slashing deprecation was mentioned a couple of times in the Discord channel.
On the flip side, the fact remains that the slashing mechanism is part of the audit scope, and was counted towards the sLOC, plus the points I raised above. There wasn't an indication in the code that slashing had been deprecated.
It's a difficult decision here, considering the different parties' POV. There could be some wardens that didn't submit issues related to slashing because of what was brought up in the Discord channel, and those that did (as one can see referenced above) because they didn't monitor the channel.