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: 31/71
Findings: 2
Award: $268.73
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 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
216.6637 USDT - $216.66
safeApprove
may revert for non-zero to non-zero approvalsThis issue impacts two contracts:
OpenZeppelin’s safeApprove reverts for non-0 to non-0 approvals. This is considered in a few places where safeApprove is performed twice with the first one for 0 and then for the desired allowance. However, there are uses of safeApprove where it is called only once for the desired allowance but it is not clear that the current allowance is guaranteed to be zero. In such cases, safeApprove may revert.
It can happen for some reasons. For example, if reward contract has some kind of max stake amount, stakeFor()
will not use up all approved amount => allowance is non-zero next time user deposit.
Manual Review
Approve to 0 first then approve the desired amount
IERC20(token).safeApprove(rewardContract, 0);
earmarkFees()
will not work if fee params is set incorrectlyIn Booster.earmarkFees()
function, _lockFeesIncentive
and _stakerLockFeesIncentive
is calculated using balance of Booster.
There is no check when fee manager set lockFeesIncentive
and stakerLockFeesIncentive
.
If fee params is set incorrectly (i.e lockFeesIncentive + stakerLockFeesIncentive != FEE_DENOMINATOR
), there are 2 cases
lockFeesIncentive + stakerLockFeesIncentive > FEE_DENOMINATOR
Total fee will bigger than balance of Booster => safeTransfer()
revert.
lockFeesIncentive + stakerLockFeesIncentive < FEE_DENOMINATOR
Some fee will be left in contract => Have to call earmarkFees()
again and again to claim the rest.
lockFeesIncentive = 10000 stakerLockFeesIncentive = 10 FEE_DENOMINATOR = 10000
_lockFeesIncentive = balance * lockFeesIncentive / FEE_DENOMINATOR = 1000000 _stakerLockFeesIncentive = balance * stakerLockFeesIncentive / FEE_DENOMINATOR = 1000000 = 1000
Now, _lockFeesIncentive + _stakerLockFeesIncentive = 1001000 > balance
. Booster doesn’t have enough fee token so safeTransfer
will revert.
Manual Review
Add check when when fee manager set fee params
require(lockFeesIncentive + stakerLockFeesIncentive == FEE_DENOMINATOR);
In inverse loop, stop condition is i + 1 != 0
but this condition will revert instead of normally stopping the for loop.
Index variable i
is unsigned integer. So when i + 1 == 0
, value of i
should be -1
which is impossible (i is unsigned). Instead, it will revert because of underflow.
Impacted codes
Manual Review
Review and change the stop condition of inverse loops.
unlockTime
is increased every week, implementation is not align with documentationIn VeAssetDepositor._lockVeAsset()
function, the comment says "increase time too if over 2 week buffer", but actually it will be increased every week.
This will not caused any loss of money, but it will make users confuse, misunderstand and cost users more gas to execute TX.
1648814400
(Friday, April 1, 2022 12:00:00 PM).unlockAt = block.timestamp + MAXTIME = 1648814400 + 125798400 = 1774612800
unlockInWeeks = (unlockAt/WEEK)*WEEK = (1774612800 / 604800) * 604800 = 2934 * 604800 = 1774483200
Assume that unlockTime
will be updated to unlockInWeeks
: unlockTime = 1774483200
2. Bob deposited at timestamp 1649332800
(Thursday, April 7, 2022 12:00:00 PM) - Thursday next week.
unlockAt = block.timestamp + MAXTIME = 1649332800 + 125798400 = 1775131200
unlockInWeeks = (unlockAt/WEEK)*WEEK = (1775131200 / 604800) * 604800 = 2935 * 604800 = 1775088000
Now because unlockInWeeks.sub(unlockTime) = 604800 > 2
, unlockTime
will be updated even though it's less than a week (from Friday to Thursday next week)
Should check if unlockInWeeks.sub(unlockTime) > 2*WEEK
Since you use solidity version 0.8, can just remove SafeMath library and replace add, sub, mul, div
to normal operator +, -, *, /
because all arithmetic operations will revert on underflow and overflow from solidity 0.8.
https://docs.soliditylang.org/en/v0.8.13/080-breaking-changes.html
Remove SafeMath library and update math operator.
#0 - GalloDaSballo
2022-07-08T00:27:10Z
Invalid transferFrom
always uses the entire amount
Valid Low
##Â 3. Inverse loop is implemented incorrectly Valid Low, Nice find
Valid low
Valid Refactoring
Personally I really dislike the format which makes the report needlessly long, I'd recommend the warden check other reports which are more concise.
That said, findings were solid, good work.
3L, 1R
🌟 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
52.0655 USDT - $52.07
nextEpochDate
instead of reading from storage.In VE3DLocker._checkpointEpoch()
, there is a while loop to fill epoch gaps until the next epoch date. Each iter, it reads from storage 4 times (2 in the while condition, 2 when calculate nextEpochDate
).
In fact, nextEpochDate
can be kept track simply by adding rewardsDuration
each epoch.
Use variable nextEpochDate
to cache like this
uint256 nextEpochDate = uint256(epochs[epochs.length - 1].date); while (nextEpockDate != currentEpoch) { uint256 nextEpochDate = nextEpockDate.add(rewardsDuration); epochs.push(Epoch({ supply: 0, date: uint32(nextEpochDate) })); }
rewardPerTokenStored
In VE3DRewardPool.updateReward()
, rewardPerTokenStored
is written then read from storage. It can be cached from rewardPerToken()
to save gas
Cache rewardPerTokenStored
like this
uint256 _cached = rewardPerToken(_rewardToken); rewardTokenInfo[_rewardToken].rewardPerTokenStored = _cached; … … rewardTokenInfo[_rewardToken].userRewardPerTokenPaid[account] = _cached;
In VE3DRewardPool.notifyRewardAmount()
function, rewardTokenInfo[_rewardToken]
is used repeatedly (9 times). Each time, it require some gas to calculate the storage address of rewardTokenInfo[_rewardToken]
(keccak256).
Instead we can keep a storage variable, so it only needs to calculate the storage address once only.
Impacted functions: https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VE3DRewardPool.sol#L275
Reference from solidity docs https://docs.soliditylang.org/en/v0.8.13/internals/layout_in_storage.html#mappings-and-dynamic-arrays
Keep storage variable to save gas.
RewardTokenInfo storage _tmp = rewardTokenInfo[_rewardToken];
#0 - GalloDaSballo
2022-07-18T23:21:07Z
##Â 1. Save gas by keeping track of nextEpochDate instead of reading from storage. Roughly 100 gas saved
At least 100 gas saved (more most of the time)
3 * 30 = 90
Saved 290