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: 18/71
Findings: 2
Award: $1,055.99
π Selected for report: 1
π Solo Findings: 0
937.187 USDT - $937.19
https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeTokenMinter.sol#L36-L38 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/VeTokenMinter.sol#L41-L4 https://github.com/code-423n4/2022-05-vetoken/blob/2d7cd1f6780a9bcc8387dea8fecfbd758462c152/contracts/Booster.sol#L598-L614
The totalWeight
state variable of the VeTokenMinter
contract is used to work out the amount of veAsset
earned when the Booster.rewardClaimed
function is called.
However, while totalWeight
is modified inside the VeTokenMinter
contract when function updateveAssetWeight
is called, the totalWeight
is not similarly reduced when function removeOperator
is called.
The impact is that remaining operators do not receive a fair share of the total rewards and a portion of the rewards are not given out at all.
The totalWeight
is now 10.
This means that Operator 1 receives 90% of the amount while Operator 2 receives 10%.
If we then call removeOperator
on Operator 1 then 90% of the reward is no longer minted and distributed. This is unfair to the remaining operators.
The can be seen on lines 607 - 608 of the Booster
contract. Function rewardClaimed
will never be called for (removed) Operator 1. But for Operator 2 they will still receive 10% of the rewards even though Operator 1 is no longer registered in the system.
Manual Inspection
The totalWeight
should be reduced so that the remaining operators receive a fair share of the total rewards.
Using just method calls from VeTokenMinter
one could rectify this situation by
addOperator
0
using updateveAssetWeight
. This will have the effect of reducing the totalWeight
by the right amount.removeOperator
However, the removeOperator
function should just be rewritten to be as follows:
function removeOperator(address _operator) public onlyOwner { totalWeight -= veAssetWeights[_operator]; veAssetWeights[_operator] = 0; operators.remove(_operator); }
You might also want to modify addOperator
so that a weight can be provided as an extra argument. This saves having to call addOperator
and then updateveAssetWeight
which could save on gas.
#0 - solvetony
2022-06-15T15:50:51Z
Confirmed. But this should be a middle risk.
#1 - GalloDaSballo
2022-07-24T21:30:59Z
The warden has shown how, due to a privileged call, removing an operator, the weight used to distribute rewards will not be updated fairly. This will cause an improper distribution of rewards.
Because the finding is limited to Loss of Yield, due to Admin Configuration, I believe Medium Severity to be more appropriate
π 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
118.8012 USDT - $118.80
VeAssetDepositor
there is not a whole lot of documentation. It's unclear what the escrow
address is used for.operators.add
in VeTokenMinter.addOperator
function addOperator(address _newOperator) public onlyOwner { operators.add(_newOperator); }
The return value is not checked so you will not know if addOperator
was successful or not. It is recommended that either you do this or return the value to the contract caller. e.g.
function addOperator(address _newOperator) public onlyOwner returns (bool) { return operators.add(_newOperator); }
VeTokenMinter.mint
to improve understandingThe variables names reduction
and reductionPerCliff
were very confusing. Also, they both contain the word "reduction" but mean very different things.
reductionPerCliff
would be better described as cliffWidth
. It's the "width" of each cliff because it is equal to maxSupply.div(totalCliffs)
.reduction
would be better described as cliffsLeft
as it is a the number of cliffs until totalCliffs
is reached. This is because, as cliff
increases from 0
to totalCliffs
, cliffsLeft
decreases from totalCliffs
to 0
.You could rewrite VE3DRewardPool.update_reward
as follows:
modifier updateReward(address account) { address _rewardToken; for (uint256 i = 0; i < rewardTokens.length(); i++) { _rewardToken = rewardTokens.at(i); RewardTokenInfo memory rti = rewardTokenInfo[_rewardToken]; rti.rewardPerTokenStored = rewardPerToken(_rewardToken); rti.lastUpdateTime = lastTimeRewardApplicable(_rewardToken); if (account != address(0)) { rti.rewards[account] = earnedReward(_rewardToken, account); rti.userRewardPerTokenPaid[account] = rti.rewardPerTokenStored; } } _; }
By declaring RewardToken memory rti
you avoid repeated uses of rewardTokenInfo[_rewardToken]
. It's easier on the eyes and saves gas.
This can be done for several other function in the VE3DRewardPool
contract.
RewardPaid
event to include rewardToken
addressThe VE3DRewardPool.getReward
function emits a RewardPaid
event just like the original Convex code did. However, it does this once for each reward token. Perhaps it would be a good idea to add an extra field to tell people which reward token the reward was paid for? e.g.
event RewardPaid(address indexed user, address indexed rewardToken, uint256 reward);
The same is suggested for the RewardPaid
event.
totalSupply
should be updated before safeTransfer
in VeTokenMinter.mint
At the end of the mint
function we have:
veToken.safeTransfer(_to, _amount); totalSupply += _amount;
This violates the check-effects-interactions pattern which prevents possible re-entrancy attacks. Swapping these two lines will means that the "interactions" (with the veToken
contract) occurs after the "effect" (totalSupply += amount
)
VeAssetDepositor._lockVeAsset
means that locking can occur more often than every two weeksThe arithmetic on line VeAssetDepositor.sol:106 is incorrect. The unlockInWeeks
variable has units of seconds not weeks so the expression should actually be:
unlockInWeeks.sub(unlockTime) > 2 * WEEK
This could lead to tokens being locked more often that when the two week window has passed. A Proof of Concept has been written here.
However, discussions with the sponsor lead this warden to only assign this as low risk since there is not much downside to locking the tokens more often that every two weeks, except possibly that there is higher gas costs.
#0 - GalloDaSballo
2022-07-09T18:28:53Z
I disagree as the operations always ensure the address is added, either it get's added on "this" call or it was already added
Valid Refactor
Valid Ref
NC
Valid Low
Valid Low
2L 2R 1NC