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: 26/30
Findings: 1
Award: $27.00
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: c3phas
Also found by: 0x11singh99, 0x3b, 0xta, JCK, K42, ReyAdmirado, SAQ, Sathish9098, hunter_w3b, kaveyjoe, lsaudit, turvy_fuzz
27.0048 USDC - $27.00
delegatorCumulativeStakeAt
can be optimized to save gasif (rewardRound < bond.lastClaimRound) { // If the transcoder hasn't called reward() since the last time the delegator claimed earnings, there wil be // no rewards to add to the delegator's stake so we just return the originally bonded amount. return bond.bondedAmount; }
Function delegatorCumulativeStakeAt
calculates cumulative stake of a delegator at any given round. Above condition checks if the transcoder hasn't called reward() since the last time the delegator claimed earnings. If he did (so rewardRound < bond.lastClaimRound
), then there is no reward to add to the delegator's stake and no need to calculate it once again. We can just return the originally bonded amount
This condition, however, does not take into a consideration a scenario, when rewardRound == bond.lastClaimRound
. In this case, the start and the end pools would be the same, so we don't need to calculate cumulative rewards once again. In that case, bond.bondedAmount
should be returned also, when rewardRound == bond.lastClaimRound
.
To save some gas, we should include this scenario in the above if-condition. This line: if (rewardRound < bond.lastClaimRound)
should be changed to: if (rewardRound <= bond.lastClaimRound)
.
unchecked
revert FutureLookup(_round, currentRound == 0 ? 0 : currentRound - 1);
When currentRound
is 0, condition will return 0. Otherwise, currentRound - 1
won't overflow, since currentRound
is at least 1.
156: (uint256 amount, ) = getBondingStateAt(_account, clock() + 1); 182: return getTotalActiveStakeAt(clock() + 1); 206: (, address delegateAddress) = getBondingStateAt(_account, clock() + 1); 267: if (_startRound != clock() + 1) { 268: revert InvalidStartRound(_startRound, clock() + 1); 326: if (_round > clock() + 1) { 327: revert FutureLookup(_round, clock() + 1); 427: if (_round > clock() + 1) { 428: revert FutureLookup(_round, clock() + 1); 168: (uint256 amount, ) = getBondingStateAt(_account, _round + 1); 195: return getTotalActiveStakeAt(_round + 1); 219: (, address delegateAddress) = getBondingStateAt(_account, _round + 1);
clock() + 1
and _round + 1
can be wrapped by unchecked
block. It's almost impossible that round
or clock()
will ever overflow (this would require almost impossible to achieve amount of time to have round
or clock()
such big, that +1 will overflow it). Moreover, if those were so big and in a risk of overflow, the protocol would be broken - because achieving next round wouldn't be possible. Thus we can assume, that those values won't ever overflow and wrap them into unchecked
block.
Assuming that function call which might revert does not interfere with other operations, move them as high possible
checkpoints.data[_startRound] = bond; // now store the startRound itself in the startRounds array to allow us // to find it and lookup in the above mapping checkpoints.startRounds.pushSorted(_startRound);
pushSorted()
might revert when incorrect _startRound
will be passed to it. To save some gas (in case pushSorted()
will revert), you can move checkpoints.startRounds.pushSorted(_startRound);
above checkpoints.data[_startRound] = bond;
.
totalStakeCheckpoints.data[_round] = _totalStake; totalStakeCheckpoints.rounds.pushSorted(_round);
pushSorted()
might revert when incorrect _round
will be passed to it. To save some gas (in case pushSorted()
will revert), you can call totalStakeCheckpoints.rounds.pushSorted(_round);
before totalStakeCheckpoints.data[_round] = _totalStake;
.
bool isTranscoder = delegateAddress == _account; if (bond.bondedAmount == 0) { amount = 0; } else if (isTranscoder) {
Since isTranscoder
is used only once, instead of wasting gas on bool isTranscoder
declaration, _account == delegate
can be used directly:
} else if (delegateAddress == _account) {
bool isTranscoder = newDelegate == _account; bool wasTranscoder = previousDelegate == _account; // we want to register zero "delegate votes" when the account is/was not a transcoder uint256 previousDelegateVotes = wasTranscoder ? previous.delegatedAmount : 0; uint256 currentDelegateVotes = isTranscoder ? current.delegatedAmount : 0;
The same issue occurs with isTranscored
and wasTranscored
which might be used directly:
uint256 previousDelegateVotes = previousDelegate == _account ? previous.delegatedAmount : 0; uint256 currentDelegateVotes = newDelegate == _account ? current.delegatedAmount : 0;
unchecked
blockrequire(fees >= _amount, "insufficient fees to withdraw"); delegators[msg.sender].fees = fees.sub(_amount);
The requirement statement guarantees that fees >= _amount
, thus fees - _amount
will never overflow. Instead of calling fees.sub(_amount)
, unchecked
block can be used.
511: currentRound.add(1), 573: del.startRound = currentRound.add(1); 585: del.startRound = currentRound.add(1); 729: newDel.startRound = currentRound.add(1); 827: delegators[msg.sender].startRound = roundsManager().currentRound().add(1); 1270: uint256 startRound = del.lastClaimRound.add(1); 1320: uint256 nextRound = currRound.add(1); 1365: uint256 nextRound = currRound.add(1); 1444: uint256 deactivationRound = roundsManager().currentRound().add(1); 1506: uint256 startRound = _lastClaimRound.add(1);
It's almost impossible that currentRound
will ever overflow (this would require almost impossible to achieve amount of time to have currentRound
such big value, that +1 will overflow it). Moreover, if those values were so big and in a risk of overflow, the protocol would be broken - because achieving next round wouldn't be possible. Thus we can assume, that those values won't ever overflow and instead of using .add(1)
, we can use + 1
wrapped in unchecked
block.
Delegator storage del = delegators[msg.sender]; UnbondingLock storage lock = del.unbondingLocks[_unbondingLockId]; require(isValidUnbondingLock(msg.sender, _unbondingLockId), "invalid unbonding lock ID"); require( lock.withdrawRound <= roundsManager().currentRound(), "withdraw round must be before or equal to the current round" );
require conditions which might revert earlier can be moved on top:
require(isValidUnbondingLock(msg.sender, _unbondingLockId), "invalid unbonding lock ID"); UnbondingLock storage lock = del.unbondingLocks[_unbondingLockId]; require( lock.withdrawRound <= roundsManager().currentRound(), "withdraw round must be before or equal to the current round" ); Delegator storage del = delegators[msg.sender];
uint256 currentRound = roundsManager().currentRound(); require(isActiveTranscoder(msg.sender), "caller must be an active transcoder");
require condition which might revert can be moved on top:
require(isActiveTranscoder(msg.sender), "caller must be an active transcoder"); uint256 currentRound = roundsManager().currentRound();
Delegator storage del = delegators[_delegator]; UnbondingLock storage lock = del.unbondingLocks[_unbondingLockId]; require(isValidUnbondingLock(_delegator, _unbondingLockId), "invalid unbonding lock ID");
require condition which might revert can be moved on top:
require(isValidUnbondingLock(_delegator, _unbondingLockId), "invalid unbonding lock ID"); Delegator storage del = delegators[_delegator]; UnbondingLock storage lock = del.unbondingLocks[_unbondingLockId];
uint256 totalVotes = againstVotes + forVotes + abstainVotes; return totalVotes >= quorum(proposalSnapshot(_proposalId));
Since totalVotes
is used only once, instead of wasting gas on uint256 totalVotes
declaration, againstVotes + forVotes + abstainVotes
can be used directly:
return againstVotes + forVotes + abstainVotes >= quorum(proposalSnapshot(_proposalId));
uint256 opinionatedVotes = againstVotes + forVotes; return forVotes >= MathUtils.percOf(opinionatedVotes, quota);
Since opinionatedVotes
is used only once, instead of wasting gas on uint256 opinionatedVotes
declaration, againstVotes + forVotes
can be used directly:
return forVotes >= MathUtils.percOf(againstVotes + forVotes, quota);
bool isTranscoder = _account == delegate; if (isTranscoder) {
Since isTranscoder
is used only once, instead of wasting gas on bool isTranscoder
declaration, _account == delegate
can be used directly:
if (_account == delegate) {
if (_supportInt > uint8(VoteType.Abstain)) { revert InvalidVoteType(_supportInt); } VoteType support = VoteType(_supportInt); ProposalTally storage tally = _proposalTallies[_proposalId]; ProposalVoterState storage voter = tally.voters[_account]; if (voter.hasVoted) { revert VoteAlreadyCast(); }
VoteType support = VoteType(_supportInt);
line can be moved after the second revert:
if (_supportInt > uint8(VoteType.Abstain)) { revert InvalidVoteType(_supportInt); } ProposalTally storage tally = _proposalTallies[_proposalId]; ProposalVoterState storage voter = tally.voters[_account]; if (voter.hasVoted) { revert VoteAlreadyCast(); } VoteType support = VoteType(_supportInt);
_handleVoteOverrides
if (delegateSupport == VoteType.Against) { _tally.againstVotes -= _weight; } else if (delegateSupport == VoteType.For) { _tally.forVotes -= _weight; } else { assert(delegateSupport == VoteType.Abstain); _tally.abstainVotes -= _weight; }
assert(delegateSupport == VoteType.Abstain)
is not needed, because if delegateSupport
is not VoteType.Against
(first if-condition), is not VoteType.For
(second else-if condition), then it must be VoteType.Abstain
.
Function _handleVoteOverrides
is called by _countVote
and inside _countVote
there is already a check that:
if (_supportInt > uint8(VoteType.Abstain)) { revert InvalidVoteType(_supportInt); }
This implies, that when we reach else-condition, then delegateSupport
must be VoteType.Abstain
. The assert
is not needed.
array.length
is calculated twiceif (array.length == 0) { array.push(val); } else { uint256 last = array[array.length - 1];
Cache the value of array.length
instead of calculating it twice.
PreciseMathUtils.percPoints(1, 1)
can be pre-calculated before contract deploymentSince PreciseMathUtils.percPoints(1, 1)
contains constant value, its value can be calculated before compiling and deploying the contract.
: PreciseMathUtils.percPoints(1, 1);
: PreciseMathUtils.percPoints(1, 1);
_startPool.cumulativeRewardFactor = PreciseMathUtils.percPoints(1, 1);
_endPool.cumulativeRewardFactor = PreciseMathUtils.percPoints(1, 1);
#0 - c4-judge
2023-09-21T10:44:15Z
HickupHH3 marked the issue as grade-b
#1 - HickupHH3
2023-09-21T10:44:29Z
decent recommendations