Livepeer Onchain Treasury Upgrade - K42's results

Decentralized video infrastructure protocol powering video in web3's leading social and media applications.

General Information

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

Livepeer

Findings Distribution

Researcher Performance

Rank: 29/30

Findings: 1

Award: $27.00

Gas:
grade-b

🌟 Selected for report: 0

🚀 Solo Findings: 0

Findings Information

🌟 Selected for report: c3phas

Also found by: 0x11singh99, 0x3b, 0xta, JCK, K42, ReyAdmirado, SAQ, Sathish9098, hunter_w3b, kaveyjoe, lsaudit, turvy_fuzz

Labels

bug
G (Gas Optimization)
grade-b
G-13

Awards

27.0048 USDC - $27.00

External Links

Gas Optimization Report for Livepeer by K42

Possible Optimization in BondingManager.sol

Possible Optimization 1 =

After Optimization:

require(accessControlBitmap[msg.sender] & ROLE_TICKET_BROKER != 0, "unauthorized");

Estimated Gas Saved = Approximately 800 gas.

Possible Optimization 2 =

  • In _currentRoundInitialized, use a cache that updates every N blocks to avoid frequent external calls. Make sure to ensure that the value of N is chosen carefully to balance gas savings and data freshness.

After Optimization:

if (block.number > lastRoundBlock + N) {
    currentRoundInitialized = roundsManager().currentRoundInitialized();
    lastRoundBlock = block.number;
}

Estimated Gas Saved = Approximately 1500 gas.

Possible Optimization in BondingVotes.sol

Possible Optimization =

After Optimization:

bool roleChanged = (previousDelegate ^ newDelegate) != 0;
if (roleChanged) {
    emit DelegateChanged(_account, previousDelegate, newDelegate);
}
  • Estimated gas saved = Approximately 500 gas, due to reducing the number of EQ and ISZERO opcodes by using bitwise XOR.

Possible Optimizations in EarningsPoolLIP36.sol

Possible Optimizations =

After Optimization for updateCumulativeFeeFactor():

// Early return if _fees is zero, avoiding unnecessary calculations
if (_fees == 0) return;

// Initialize cumulativeFeeFactor if it's zero
if (earningsPool.cumulativeFeeFactor == 0) {
    earningsPool.cumulativeFeeFactor = prevCumulativeFeeFactor;  // Use the previous factor directly
}

// Update the cumulativeFeeFactor
earningsPool.cumulativeFeeFactor = earningsPool.cumulativeFeeFactor.add(
    PreciseMathUtils.percOf(prevCumulativeRewardFactor, _fees, earningsPool.totalStake)
);

After Optimization for updateCumulativeRewardFactor():

// Early return if _rewards is zero, avoiding unnecessary calculations
if (_rewards == 0) return;

// Initialize cumulativeRewardFactor if it's zero
if (earningsPool.cumulativeRewardFactor == 0) {
    earningsPool.cumulativeRewardFactor = prevCumulativeRewardFactor;  // Use the previous factor directly
}

// Update the cumulativeRewardFactor
earningsPool.cumulativeRewardFactor = earningsPool.cumulativeRewardFactor.add(
    PreciseMathUtils.percOf(prevCumulativeRewardFactor, _rewards, earningsPool.totalStake)
);
  • Estimated gas saved = Early exit when _fees == 0: ~5000 gas (due to avoided SSTORE), avoiding ADD operation: ~3 gas = Total: ~5003 gas. Therefore for both potential 10006 gas saved.

Possible Optimizations in SortedArrays.sol

Possible Optimization 1 =

  • You can eliminate the use of the last variable in the pushSorted() function by directly comparing val with the last element of the array.

After Optimization:

// Directly compare with the last element, eliminating the need for a separate variable.
if (val < array[array.length - 1]) {
    revert DecreasingValues(val, array[array.length - 1]);
}

if (val != array[array.length - 1]) {
    array.push(val);
}
  • Estimated gas saved = Around ~5-10 gas.

Possible Optimization 2 =

  • You can use a single if statement with logical OR (||) to combine conditions in pushSorted().

After Optimization:

// Combine conditions using logical OR to reduce the number of conditional jumps.
if (val < array[array.length - 1] || val != array[array.length - 1]) {
    revert DecreasingValues(val, array[array.length - 1]);
} else {
    array.push(val);
}
  • Estimated gas saved = Around ~10-20 gas (reducing the number of conditional jumps).

Possible Optimization in GovernorCountingOverridable.sol

Possible Optimization =

  • In the ProposalTally struct, you can use a single uint256 to store all three vote counts (againstVotes, forVotes, abstainVotes) by partitioning the uint256 into three 85-bit segments. This will reduce the storage space and consequently the gas for storage operations.

After Optimization in the ProposalTally struct:

struct ProposalTally {
    uint256 voteCounts; // againstVotes in bits 0-84, forVotes in bits 85-169, abstainVotes in bits 170-254
    mapping(address => ProposalVoterState) voters;
}

Then update _countVote() like so:

// Use bitwise operations to update the vote counts.
uint256 shift = uint256(support) * 85;
uint256 mask = uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) >> (256 - 85);
tally.voteCounts = (tally.voteCounts & ~(mask << shift)) | ((_weight & mask) << shift);

Then update proposalVotes() like so:

function proposalVotes(uint256 _proposalId)
    public
    view
    returns (
        uint256 againstVotes,
        uint256 forVotes,
        uint256 abstainVotes
    )
{
    uint256 voteCounts = _proposalTallies[_proposalId].voteCounts;
    uint256 mask = uint256(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) >> (256 - 85);
    
    againstVotes = (voteCounts >> 0) & mask;
    forVotes = (voteCounts >> 85) & mask;
    abstainVotes = (voteCounts >> 170) & mask;
}

Then update _quorumReached():

function _quorumReached(uint256 _proposalId) internal view override returns (bool) {

    (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) = proposalVotes(_proposalId);
    uint256 totalVotes = againstVotes + forVotes + abstainVotes;
    return totalVotes >= quorum(proposalSnapshot(_proposalId));
}

Finally the _voteSucceeded() Function:

function _voteSucceeded(uint256 _proposalId) internal view override returns (bool) {
    (uint256 againstVotes, uint256 forVotes, ) = proposalVotes(_proposalId);
    uint256 opinionatedVotes = againstVotes + forVotes;
    return forVotes >= MathUtils.percOf(opinionatedVotes, quota);
}
  • Estimated gas saved = Around ~28000-29000 gas (considering both SLOAD and SSTORE optimizations) This optimization is safe as long as the individual vote counts do not exceed 2^85 − 1, which is a very large number and unlikely to be reached.

Possible Optimization in LivepeerGovernor.sol

Possible Optimization =

  • The initialize() function sets multiple state variables. These could be batched into a struct to reduce the gas cost of the stack operations.

After Optimization:

// Define a struct for initialization parameters
struct InitParams {
    uint256 initialVotingDelay;
    uint256 initialVotingPeriod;
    uint256 initialProposalThreshold;
    uint256 initialQuorum;
    uint256 quota;
}

// Modify initialize function to accept the struct
function initialize(InitParams memory params) public initializer {
    __Governor_init("LivepeerGovernor");
    __GovernorSettings_init(params.initialVotingDelay, params.initialVotingPeriod, params.initialProposalThreshold);
    __GovernorTimelockControl_init(treasury());
    __GovernorVotes_init(votes());
    __GovernorVotesQuorumFraction_init(params.initialQuorum);
    __GovernorCountingOverridable_init(params.quota);
}
  • Estimated gas saved = Around ~500-1000 gas for reduced stack operations.

#0 - c4-judge

2023-09-21T10:46:22Z

HickupHH3 marked the issue as grade-b

AuditHub

A portfolio for auditors, a security profile for protocols, a hub for web3 security.

Built bymalatrax © 2024

Auditors

Browse

Contests

Browse

Get in touch

ContactTwitter