Security Vulnerability Disclosure (Fixed): Transcoders Could Lose Out On Rewards/Fees Under Certain Conditions

Find below a description of a security vulnerability with the BondingManager contract that was surfaced after the LIP-36 security audit conducted by Quantstamp (thanks to Quantstamp for highlighting an issue during the LIP-36 security audit that allowed us to discover this vulnerability in the already deployed mainnet contracts!).

This vulnerability has been fixed and users do not need to worry about the vulnerability impacting them. The technical details of the vulnerability are shared for informational purposes.

Transcoders who miss a reward call and that do not receive stake updates in a given round may lose out on rewards/fees in the following round if delegation activity occurs prior to a reward call or receiving fees

Impact

What kind of vulnerability is it? Who is impacted?

Description

This vulnerability can cause a transcoder and its delegators to miss out on new rewards and/or fees in the current round if the transcoder did not receive any stake updates in the previous round (either via delegation or by calling reward) and delegation activity for the transcoder occurs before the transcoder calls reward and/or receives fees in the current round.

The root cause of this vulnerability is that when rewardWithHint() is called, the setStake() function is only called for the current round’s earnings pool if the transcoder’s lastActiveStakeUpdateRound is in the past. A transcoder’s lastActiveStakeUpdateRound is updated in the internal functions increaseTotalStake() and decreaseTotalStake() which are called whenever a transcoder receives stake updates either via delegation activity (i.e. calls to bond(), unbond(), rebond()) or calls to rewardWithHint(). If a transcoder receives stake updates in the current round, then its lastActiveStakeUpdateRound is set to current round + 1 to indicate that the transcoder’s active stake has been updated for current round + 1 (stake updates in the current round only impacts the active stake of a transcoder in current round + 1). If there is delegation activity for a transcoder before rewardWitHint() is called, then when rewardWithHint() is called lastActiveStakeUpdateRound will be in the future. This is ok if the transcoder received stake updates in the previous round because at this point setStake() would have been called for the current round’s earnings pool when either increaseTotalStake() or decreaseTotalStake() were called in the previous round. However, in this situation, if the transcoder did not receive stake updates in the previous round then setStake() function was not called in the previous round and will never be called for the current round because the transcoder’s lastActiveStakeUpdateRound would’ve already been set to a round in the future.

setStake() not being called for the current round’s earnings pool in rewardWithHint() has the following consequences:

  • The Minter.createReward() call in rewardWithHint() will treat the transcoder’s active stake as 0 which results in 0 rewards minted for the transcoder
  • When a transcoder or its delegators claims earnings, the fee pool share will be 0 because the claimableStake field of the earnings pool will be set to 0

Triggering/Reproducting

The pre-conditions that need to hold for this vulnerability to be triggered are:

  • The transcoder did not receive any stake updates in the previous round
  • There is delegation activity for a transcoder in the current round before reward is called or before fees are received

Exploit Scenarios

This vulnerability could be triggered over the course of normal day-to-day operations if a transcoder misses a reward call. It is normal for there to be no delegation activity in a day. So, if a transcoder misses a reward call in the prevous round and there happens to be delegation activity in the current round and a transcoder was too slow to call reward (possibly deliberately to wait for better gas prices) then this vulnerability could be triggered.

This vulnerability could also be triggered by a malicious actor that is seeking to grief others. A malicious actor could watch for transcoders that miss reward calls in the previous round and that did not receive any staking updates in the previous round. Then, the actor could front-run any reward calls from the transcoder in the current round by staking 1 base unit LPT to the transcoder. With this example in mind, it is worth noting that there is little in-protocol economic benefit (excluding extra-protocol economic benefits out of this analysis) from doing this besides for the sake of griefing the transcoder and its delegators. The malicious actor doesn’t gain any additional value from triggering this vulnerability and in fact it actually potentially loses out on rewards/fees that it would otherwise earn if it did not trigger this vulnerability.

Patches

Has the problem been patched? What versions should users upgrade to?

The vulnerability has been patched with the following commit:

Deployment

These were the steps taken for deployment:

gitCommitHash: 0xcb5548cc1496607da341804d55fd4a90038d2687

  • This is the Git commit hash with the fix

contractId("BondingManagerTarget"): 0xfc6f6f33d2bb065ac61cbdd4dbe4b7adf6f3e7e6c6a3d1fe297cbf9a187092e4

  1. Deploy a new BondingManager implementation contract at bondingManagerAddr
  2. Register the new BondingManager implementation contract by calling the following on the Controller:
    • setContractInfo(contractId("BondingManagerTarget"), bondingManagerAddr, gitCommitHash)
  3. Merge the PR associated with this advisory into the streamflow branch

The relevant deployment transactions can be found below:

  1. Deployed a new BondingManager implementation contract
  2. Registered the new BondingManager implementation contract with the Controller

Workarounds

Is there a way for users to fix or remediate the vulnerability without upgrading?

Transcoders no longer need to worry about this vulnerability.

Transcoders could have avoided this vulnerability from being triggered by making sure to always call reward in each round that they are active because calling reward will always serve as a stake update that will trigger a setStake() call for the next round’s earnings pool.

References

Are there any links users can visit to find out more?

For more information

If you have any questions or comments about this advisory:

1 Like