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: 4/30
Findings: 2
Award: $2,417.44
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: bronze_pickaxe
2318.7003 USDC - $2,318.70
treasuryRewardCutRate
is represented as a PreciseMathUtils percPoint value. In PreciseMathUtils, the PERC_DIVISOR
is 1e27.
library PreciseMathUtils { using SafeMath for uint256; // Divisor used for representing percentages uint256 public constant PERC_DIVISOR = 10**27;
However, when calculating the treasuryRewards
, the code uses MathUrils
rather than PreciseMathUtils
.
uint256 treasuryRewards = MathUtils.percOf(rewards, treasuryRewardCutRate); rewards = rewards.sub(treasuryRewards);
Unfortunately, the PERC_DIVISOR
in MathUtils
is only 1e6.
library MathUtils { using SafeMath for uint256; // Divisor used for representing percentages uint256 public constant PERC_DIVISOR = 1000000;
As a result, any call to function updateTranscoderWithFees
will receive a larger treasuryRewards
due to decimal issues. Specifically, if treasuryRewardCutRate
is set to 50%, then it's 50 * 1e27
, so the value of treasuryRewards
we get is reward * 50 * 1e27 / 1e6
, which is larger than the original rewards
.
So rewards.sub(treasuryRewards)
will directly underflow, causing the entire contract to malfunction.
When calling updateTranscoderWithFees
, if the statement (prevEarningsPool.cumulativeRewardFactor == 0 && lastRewardRound == currentRound)
is met and rewards
is not zero, once treasuryRewardCutRate
is bigger than 1% (10*27), then the treasuryRewards
will be bigger than rewards
, which will revert because of the underflow.
To make poc more simple, I patch the source code in BondingManager.sol
, I comment out the prevEarningsPool.cumulativeRewardFactor == 0
and let the rewards
be 1e18. Note that it doesn't affect the original logic of the code, it's just for testing convenience.
diff --git a/contracts/bonding/BondingManager.sol b/contracts/bonding/BondingManager.sol index 93e06ba..9f179f5 100644 --- a/contracts/bonding/BondingManager.sol +++ b/contracts/bonding/BondingManager.sol @@ -340,7 +340,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager { } uint256 totalStake = earningsPool.totalStake; - if (prevEarningsPool.cumulativeRewardFactor == 0 && lastRewardRound == currentRound) { + if (/*prevEarningsPool.cumulativeRewardFactor == 0 && */lastRewardRound == currentRound) { // if transcoder called reward for 'currentRound' but not for 'currentRound - 1' (missed reward call) // retroactively calculate what its cumulativeRewardFactor would have been for 'currentRound - 1' (cfr. previous lastRewardRound for transcoder) // based on rewards for currentRound @@ -351,6 +351,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager { currentRoundTotalActiveStake ); + rewards = 1000000000000000000; // Deduct what would have been the treasury rewards uint256 treasuryRewards = MathUtils.percOf(rewards, treasuryRewardCutRate); rewards = rewards.sub(treasuryRewards);
Add the test to test/unit/BondingManager.js
and run with yarn test:audit
.
diff --git a/test/unit/BondingManager.js b/test/unit/BondingManager.js index 326da04..af30fa2 100644 --- a/test/unit/BondingManager.js +++ b/test/unit/BondingManager.js @@ -5126,6 +5126,40 @@ describe("BondingManager", () => { ) }) + it.only("test updateTranscoderWithFees", async () => { + // origin treasuryRewardCutRate is zero + var x = await bondingManager.treasuryRewardCutRate(); + console.log(x); + // set next round treasuryRewardCutRate to 50% + const FIFTY_PCT = math.precise.percPoints(BigNumber.from(50), 100); + await bondingManager.setTreasuryRewardCutRate(FIFTY_PCT); + // update the treasuryRewardCutRate + await fixture.roundsManager.execute( + bondingManager.address, + functionSig("setCurrentRoundTotalActiveStake()") + ) + // new treasuryRewardCutRate is 50% + x = await bondingManager.treasuryRewardCutRate(); + console.log(x); + + await bondingManager + .connect(transcoder) + .rewardWithHint( + ethers.constants.AddressZero, + ethers.constants.AddressZero + ) + + // trigger the revert + await fixture.ticketBroker.execute( + bondingManager.address, + functionEncodedABI( + "updateTranscoderWithFees(address,uint256,uint256)", + ["address", "uint256", "uint256"], + [transcoder.address, 1000, currentRound + 1] + ) + ) + + }) it("should update transcoder's pendingFees when transcoder claims earnings before fees are generated", async () => { await bondingManager .connect(transcoder)
yarn
Use the right Math
diff --git a/contracts/bonding/BondingManager.sol b/contracts/bonding/BondingManager.sol index 93e06ba..425179b 100644 --- a/contracts/bonding/BondingManager.sol +++ b/contracts/bonding/BondingManager.sol @@ -352,7 +352,7 @@ contract BondingManager is ManagerProxyTarget, IBondingManager { ); // Deduct what would have been the treasury rewards - uint256 treasuryRewards = MathUtils.percOf(rewards, treasuryRewardCutRate); + uint256 treasuryRewards = PreciseMathUtils.percOf(rewards, treasuryRewardCutRate); rewards = rewards.sub(treasuryRewards); uint256 transcoderCommissionRewards = MathUtils.percOf(rewards, earningsPool.transcoderRewardCut);
Math
#0 - c4-pre-sort
2023-09-08T15:11:27Z
141345 marked the issue as duplicate of #165
#1 - c4-judge
2023-09-18T02:44:41Z
HickupHH3 marked the issue as satisfactory
🌟 Selected for report: catellatech
Also found by: 0x3b, ADM, Banditx0x, JayShreeRAM, Krace, Sathish9098
98.7434 USDC - $98.74
Due to the nature of encoding, the cost of video streaming transmission is quite high, which significantly constrains the growth of many video startups. Therefore, video infrastructure needs a more scalable and cost-effective solution to keep up with this growth. Livepeer is designed to reduce the cost of video streaming transmission. Anyone could become an Orchestrator and contribute their computer resources in service of transcoding and distributing video, which will bring them fees. Orchestrator could also stake their LPT token to let delegators participate in this process.
To evaluate the Livepeer web3 protocol's Solidity codebase, I followed a systematic and thorough approach.
The BondingManager
added three variables:
treasuryRewardCutRate
: the % of rewards should be routed to the treasury. When rewardWithHint
is called, the % of rewards will be transferred into the treasury.nextRoundTreasuryRewardCutRate
: next round's %treasuryBalanceCeiling
: the upper limit of treasury's LPT
It also added a modifier autoCheckpoint
, which is implemented in the contract BondingVotes
. This modifier is used to add the corresponding round and Bond info to the bondingCheckpoints
in BondingVotes
whenever the info is updated.isRegisteredTranscoder
and isActiveTranscoder
don't need to use storage, you could use memory to save the gas.
This contract primarily maintains a mapping called bondingCheckpoints
, which logs every account's BondingCheckpointsByRound
.
BondingCheckpointsByRound
records a list of checkpoints and every chekcpoint's BondingCheckpoint
info.
This contract is responsible for handling the votes of transcoders and delegators. Transcoders can vote with all their weight, while delegators, by default, follow the transcoder's vote. However, delegators can also cast their votes separately with the voting power they own, which will be deducted from the transcoder accordingly.
The only centralization risk in this project is in BondingManager
, onlyControllerOwner
allows the owner of controller to setup some sensitive parameters including unbondingPeriod
, nextRoundTreasuryRewardCutRate
, treasuryBalanceCeiling
and transcoderPool
's max size.
This contract has two main roles: transcoder and delegator. Transcoders can "hire" many different delegators to work for them and are also required to send corresponding rewards to these delegators. Additionally, transcoders can vote on behalf of their delegators, and of course, delegators can independently manage their voting choices, with the corresponding votes deducted from the transcoder's already cast votes.
Based on my understanding of the system, I think that the default ability of a transcoder to vote on behalf of all delegators might carry some risks. If a transcoder makes malicious votes and their delegators do not promptly realize it or fail to modify their own votes, there is a possibility that malicious proposals could be accepted, resulting in certain risks.
20 hours
20 hours
#0 - c4-judge
2023-09-22T03:16:14Z
HickupHH3 marked the issue as grade-b