Platform: Code4rena
Start Date: 26/05/2022
Pot Size: $75,000 USDT
Total HM: 31
Participants: 71
Period: 7 days
Judge: GalloDaSballo
Total Solo HM: 18
Id: 126
League: ETH
Rank: 24/71
Findings: 3
Award: $739.60
🌟 Selected for report: 1
🚀 Solo Findings: 0
🌟 Selected for report: WatchPug
Also found by: Dravee, TerrierLover, gzeon
379.5607 USDT - $379.56
https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DLocker.sol#L305-L329 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DLocker.sol#L349-L373 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DLocker.sol#L376-L396 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DLocker.sol#L399-L415
function totalSupplyAtEpoch(uint256 _epoch) external view returns (uint256 supply) { uint256 epochStart = uint256(epochs[_epoch].date).div(rewardsDuration).mul( rewardsDuration ); uint256 cutoffEpoch = epochStart.sub(lockDuration); //traverse inversely to make more current queries more gas efficient for (uint256 i = _epoch; i + 1 != 0; i--) { Epoch storage e = epochs[i]; if (uint256(e.date) <= cutoffEpoch) { break; } supply = supply.add(epochs[i].supply); } return supply; }
In VE3DLocker.sol
, there are multiple instances in which an inversely traverse for loop is used "to make more current queries more gas efficient".
For example:
totalSupplyAtEpoch()
balanceAtEpochOf()
pendingLockAtEpochOf()
totalSupply()
The implementation of the inversely traverse for loop is inherited from Convex's original version: https://github.com/convex-eth/platform/blob/main/contracts/contracts/CvxLockerV2.sol#L333-L334
However, Convex's locker contract is using Solidity 0.6.12, in which the arithmetic operations will overflow/underflow without revert.
As the solidity version used in the current implementation of VE3DLocker.sol
is 0.8.7
, and there are some breaking changes in Solidity v0.8.0, including:
Arithmetic operations revert on underflow and overflow.
Ref: https://docs.soliditylang.org/en/v0.8.7/080-breaking-changes.html#silent-changes-of-the-semantics
Which makes the current implementation of inversely traverse for loops always reverts.
More specifically:
for (uint i = locks.length - 1; i + 1 != 0; i--) {
will revert when locks.length == 0
at locks.length - 1
due to underflow;for (uint256 i = _epoch; i + 1 != 0; i--) {
will loop until i == 0
and reverts at i--
due to underflow.As a result, all these functions will be malfunctioning and all the internal and external usage of these function will always revert.
Change VE3DLocker.sol#L315
to:
for (uint256 i = locks.length; i > 0; i--) { uint256 lockEpoch = uint256(locks[i - 1].unlockTime).sub(lockDuration); //lock epoch must be less or equal to the epoch we're basing from. if (lockEpoch <= epochTime) { if (lockEpoch > cutoffEpoch) { amount = amount.add(locks[i - 1].amount);
Change VE3DLocker.sol#L360
to:
for (uint256 i = locks.length; i > 0; i--) { uint256 lockEpoch = uint256(locks[i - 1].unlockTime).sub(lockDuration); //return the next epoch balance if (lockEpoch == nextEpoch) { return locks[i - 1].amount; } else if (lockEpoch < nextEpoch) { //no need to check anymore break; }
Change VE3DLocker.sol#L387
to:
for (uint256 i = epochindex; i > 0; i--) { Epoch storage e = epochs[i - 1];
Change VE3DLocker.sol#L406
to:
for (uint256 i = _epoch + 1; i > 0; i--) { Epoch storage e = epochs[i - 1]; if (uint256(e.date) <= cutoffEpoch) { break; } supply = supply.add(e.supply); }
#0 - solvetony
2022-06-15T16:28:53Z
Looks valid. Also duplicates in #116 #263
#1 - GalloDaSballo
2022-07-27T00:41:13Z
The warden has shed light into a underflow that will cause a few functions which logic was ported over from 0.6.12, to revert due to new checks introduced in a more recent version of solidity.
While the bug is easily fixable, the functions are broken.
Normally I would be conflicted between a Low and Medium severity as these functions are mostly view
and seem to have no particular impact.
However, due to my familiarity with other Lockers, and the CVX Voting Strategy used I believe the finding implications are that:
So am confident in a Medium Severity and have contemplated raising to High, as impact is pretty dramatic.
Personally am very confident governance couldn't work without some of the view
functions impacted, however, considering that the Warden (and other dups) did not bring in any mention of governance, I think Medium Severity is appropriate.
I highly recommend the sponsor ensures their governance-related function don't revert as that would be very problematic
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, 0xDjango, 0xNazgul, 0xf15ers, BouSalman, Chom, Deivitto, Dravee, ElKu, FSchmoede, Funen, GimelSec, Hawkeye, MiloTruck, Picodes, SecureZeroX, SmartSek, TerrierLover, WatchPug, _Adam, asutorufos, berndartmueller, c3phas, catchup, cccz, cogitoergosumsw, cryptphi, csanuragjain, delfin454000, dipp, ellahi, gzeon, hansfriese, horsefacts, hyh, kirk-baird, minhquanym, oyc_109, pauliax, reassor, robee, sashik_eth, shenwilly, simon135, sorrynotsorry, sseefried, unforgiven, xiaoming90, z3s
296.7763 USDT - $296.78
It's a best practice to use a two step process for changes of critical settings like setOwner()
.
Lack of two-step procedure for critical operations leaves them error-prone.
function setOwner(address _owner) external { require(msg.sender == owner, "!auth"); owner = _owner; emit OwnerUpdated(_owner); }
function setFeeManager(address _feeM) external { require(msg.sender == feeManager, "!auth"); feeManager = _feeM; emit FeeManagerUpdated(_feeM); }
function setPoolManager(address _poolM) external { require(msg.sender == poolManager, "!auth"); poolManager = _poolM; emit PoolManagerUpdated(_poolM); }
function setArbitrator(address _arb) external { require(msg.sender == owner, "!auth"); rewardArbitrator = _arb; emit ArbitratorUpdated(_arb); }
function setVoteDelegate(address _voteDelegate) external { require(msg.sender == voteDelegate, "!auth"); voteDelegate = _voteDelegate; emit VoteDelegateUpdated(_voteDelegate); }
Across the contracts, there are certain critical operations that change critical values that affect the users of the protocol.
It's a best practice for these setter functions to emit events to record these changes on-chain for off-chain monitors/tools/interfaces to register the updates and react if necessary.
Instances include:
function setOwner(address _owner) external { require(msg.sender == owner, "!auth"); owner = _owner; }
function setOperator(address _operator) external { require(msg.sender == owner, "!auth"); require( operator == address(0) || IDeposit(operator).isShutdown() == true, "needs shutdown" ); operator = _operator; }
function setDepositor(address _depositor) external { require(msg.sender == owner, "!auth"); depositor = _depositor; }
function setStashAccess(address _stash, bool _status) external returns (bool) { require(msg.sender == operator, "!auth"); if (_stash != address(0)) { stashPool[_stash] = _status; } return true; }
...
uint256 _lockFeesIncentive = _balance.mul(lockFeesIncentive).div(FEE_DENOMINATOR); uint256 _stakerLockFeesIncentive = _balance.mul(stakerLockFeesIncentive).div( FEE_DENOMINATOR ); if (_lockFeesIncentive > 0) { IERC20(feeToken).safeTransfer(lockFees, _lockFeesIncentive); IRewards(lockFees).queueNewRewards(_lockFeesIncentive); } if (_stakerLockFeesIncentive > 0) { IERC20(feeToken).safeTransfer(stakerLockRewards, _stakerLockFeesIncentive); IRewards(stakerLockRewards).queueNewRewards(feeToken, _stakerLockFeesIncentive); }
function setFeeInfo(uint256 _lockFeesIncentive, uint256 _stakerLockFeesIncentive) external { require(msg.sender == feeManager, "!auth"); lockFeesIncentive = _lockFeesIncentive; stakerLockFeesIncentive = _stakerLockFeesIncentive;
lockFeesIncentive
and stakerLockFeesIncentive
are used as they are in BPS. However, there is no check to ensure that the value does not exceed 10000
.
safeApprove
has been deprecatedsafeApprove
has been deprecated in favor of safeIncreaseAllowance
and safeDecreaseAllowance
.
//stake for msg.sender IERC20(minter).safeApprove(_stakeAddress, _amount);
Can be changed to:
IERC20(minter).safeIncreaseAllowance(_stakeAddress, _amount);
#0 - GalloDaSballo
2022-07-07T00:42:52Z
Valid NC
Valid NC (Informational)
##Â [L] Lack of input validation Valid Low
In this contest, the finding is NC as the usage is appropriate from 0 to non-zero
#1 - GalloDaSballo
2022-07-07T00:43:05Z
Short and sweet, 1L, 3NC
🌟 Selected for report: IllIllI
Also found by: 0x1f8b, 0x29A, 0xKitsune, 0xNazgul, 0xf15ers, 0xkatana, Cityscape, Dravee, ElKu, FSchmoede, Funen, GalloDaSballo, Hawkeye, Kaiziron, MiloTruck, Randyyy, RoiEvenHaim, Ruhum, SecureZeroX, SmartSek, TerrierLover, TomJ, Tomio, WatchPug, Waze, _Adam, asutorufos, c3phas, catchup, cogitoergosumsw, delfin454000, ellahi, fatherOfBlocks, gzeon, hansfriese, horsefacts, jonatascm, minhquanym, oyc_109, pauliax, reassor, robee, sach1r0, saian, sashik_eth, simon135, z3s
63.261 USDT - $63.26
[S]: Suggested optimation, save a decent amount of gas without compromising readability;
[M]: Minor optimation, the amount of gas saved is minor, change when you see fit;
[N]: Non-preferred, the amount of gas saved is at cost of readability, only apply when gas saving is a top priority.
uint256 epochStart = uint256(epochs[_epoch].date).div(rewardsDuration).mul( rewardsDuration );
epochs[_epoch].date
is a multiples of rewardsDuration
, epochs[_epoch].date / rewardsDuration * rewardsDuration = epochs[_epoch].date
uint256 epochStart = uint256(epochs[_epoch].date);
findEpochId()
Implementation can be simpler and save some gasfunction findEpochId(uint256 _time) public view returns (uint256 epoch) { uint256 max = epochs.length - 1; uint256 min = 0; //convert to start point _time = _time.div(rewardsDuration).mul(rewardsDuration); for (uint256 i = 0; i < 128; i++) { if (min >= max) break; uint256 mid = (min + max + 1) / 2; uint256 midEpochBlock = epochs[mid].date; if (midEpochBlock == _time) { //found return mid; } else if (midEpochBlock < _time) { min = mid; } else { max = mid - 1; } } return min; }
Change to:
function findEpochId(uint256 _time) public view returns (uint256 epoch) { return _time.sub(epochs[0].date).div(rewardsDuration); }
Reading array length at each iteration of the loop takes 6 gas (3 for mload and 3 to place memory_offset) in the stack.
Caching the array length in the stack saves around 3 gas per iteration.
Instances include:
BaseRewardPool.sol
PoolKeeper.sol#performUpkeepMultiplePools()
IERC20 public rewardToken;
IERC20 public stakingToken;
address public operator;
address public rewardManager;
Considering these variables will never change, changing to immutable variable instead of storage variable can save gas.
uint256
variables to 0
is redundantuint256 public periodFinish = 0;
uint256 public rewardRate = 0;
uint256 public queuedRewards = 0;
uint256 public currentRewards = 0;
uint256 public historicalRewards = 0;
...
Setting uint256
variables to 0
is redundant as they default to 0
.
++i
is more efficient than i++
Using ++i
is more gas efficient than i++
, especially in a loop.
For example:
for (uint256 i = 0; i < extraRewards.length; i++)
bool
variables to false
is redundantbool public isShutdown = false;
Setting bool
variables to false
is redundant as they default to false
.
Unused events increase contract size and gas usage at deployment.
event Voted(uint256 indexed voteId, address indexed votingAddress, bool support);
Voted
is unused.
SafeMath
when necessary can save gasFor the arithmetic operations that will never over/underflow, using SafeMath will cost more gas.
For example:
if (block.timestamp >= periodFinish) { rewardRate = reward.div(duration); } else { uint256 remaining = periodFinish.sub(block.timestamp);
periodFinish - block.timestamp
will never underflow.
Change to:
if (block.timestamp >= periodFinish) { rewardRate = reward.div(duration); } else { uint256 remaining = periodFinish - block.timestamp;
#0 - GalloDaSballo
2022-07-18T23:57:11Z
4 * 2100 for immutable
Rest will save less than 500 gas
8900