Guardian's review of Node Sale for Zap, published October 2024. The report records 46 findings across 2 review rounds, including 8 critical and 13 high.
- Published
- Review window
- October 1 to 10, 2024
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Base
- Sector
- Token launches
- 8 Critical
- 13 High
- 8 Medium
- 17 Low
- 0 Informational
Scope
4 files in scope · 948 nSLOC
| File | nSLOC | Lines |
|---|---|---|
contracts/ZapToken.sol | 17 | 24 |
contracts/ZapStaking.sol | 320 | 448 |
contracts/ZapRewards.sol | 293 | 430 |
contracts/VaultVesting.sol | 318 | 476 |
Findings 46
Main Review
38 findings · October 1 to 7, 2024-
C-01 Critical DOS of importUserData DOS Resolved
Description
An attacker can DOS the import of user data by front-running with
updateVaultsInfoForUser.This increases the
nextAccountIDand causes a mismatch in accountId and indexGiven when importUserData is called by admin.The attack can be performed repeatedly at low cost (only gas cost incurred) and delays the commencement of vesting.
Recommendation
Consider allowing
updateVaultsInfoForUserto be called only after import is complete. -
C-02 Critical DOS of Bonus Weight Calculation in VaultVesting DOS Resolved
Description
calculateTotalBonusWeightingForChonkis an important function used to calculate the daily total bonus weight of users.The function requires that
nextAccountIdin ZapAccount and VaultVesting are equivalent:require(vaultCountPlusOne == nextAccountID + 2)This assumes that master accounts do not have a VaultVesting id. However, anyone can call
updateVaultsInfoForUseron either of the two master accounts which would increase VaultVesting's nextAccountId and cause this check to always fail.Recommendation
Prevent
updateVaultsInfoForUserfrom being called on master accounts:require(accountId != 1 && accountId != 2, "master accounts); -
C-03 Critical Early Unstake Fee Paid By VaultVesting Logical Error Resolved
Description
When a user unstakes their vaults early, they will have to pay an unstake fee. However, that unstake fee is simply paid by having the VaultVesting contract transfer the fee from its balance of ZAP tokens to the treasury with
vaultVesting.payEarlyUnstakingFee(fee);Consequently, a user can continuously stake and unstake to drain the VaultVesting contract out of its ZAP balance. This is especially detrimental since these ZAP tokens are used for the claiming vested tokens. Even if the protocol replenished the VaultVesting contract, a malicious user can continuously do it at the expense of cheap gas.
Recommendation
Consider requiring vault holders to pay a fee in ZAP tokens.
-
C-04 Critical Unaccounted User Staking Logical Error Resolved
Description
When
_calculateTotalTokenStakerTokens()is called, it will callgetUserTokenStakes()to determine how many tiers the user is staked in.getUserTokenStakes()will return an array with the exact amount of tiers and then iterate through the array’s length.During the iteration, it will call
getTokenStake()based off what iteration it is on and not what tier it represents. If a user stakes in tier 1 and tier 3, it will grab the stake data for tier 1 and tier 2. This will exclude the stake data from the tier they are actually staked in, which in this case is tier 3. This will lead to a user losing out on tokens that they earned.Recommendation
Loop through each tier and if the user does not have any tokens staked in that tier then
continue. -
C-05 Critical Lost Rewards Over Multiple Claims Logical Error Resolved
Description
In
claimVaults, users will claim less tokens over multiple claims.This is due to
_calculateUnlockedTokensForClaimedVaultsusingvaultsHeldas the denominator. Consider this example:- Alice has 100 locked tokens and 4 vaults
- Alice claims 2 vaults: (2/4)*100 = 50, new locked is 50
- Alice claims 1 vault: (1/4)*50=12.5 tokens, new locked is 37.5
- Alice claims her last vault: (1/4) * 37.5 = 9.4 tokens, new locked is 28.1
The remaining amount of Alice’s locked tokens cannot be claimed after.
Recommendation
Adjust the
state.standardPoolTokensLockedby(vaultsToClaim / (vaultsHeld - vaultsClaimed). -
C-06 Critical Invalid Tiers Lead To DOS Of Rewards Accrual DOS Resolved
Description
Functions
stakeTokensandstakeVaultsdo not validate that thetierIndexis a valid tier. A user could stake for an invalid tier, but still be considered an active staker. When performingsaveCalculatedVaultHolderRewardsToStatefunction_calculateEligibleStandardPoolTokensForUserwill be entered since thevaultStakeris considered active and a non-zero address is returned.Since the user does not have stakes in any of the valid tiers, the
totalStandardPoolTokensreturned will just be zero and the following would trigger a revert:require(standardPoolTokensEligibleForRewards > 0, "ZapRewards: User has no eligable rewards");Ultimately, by staking in an inappropriate tier, a user is able to DoS the accrual of standard pool and bonus pool rewards for vault holders.
Recommendation
Validate that the
tierIndexis 0-2 when staking. -
C-07 Critical Lack Of Amount Validation Leads To DOS DOS Resolved
Description
Functions
stakeTokensandstakeVaultsdo not validate that amountToStake is valid. A user could stake zero amounts but still be considered an active staker.When performing
saveCalculatedVaultHolderRewardsToStatefunction_calculateEligibleStandardPoolTokensForUserwill be entered since thevaultStakeris considered active and a non-zero address is returned.Since the user does not have staked amount, the
totalStandardPoolTokensreturned will just be zero and the following would trigger a revert:require(standardPoolTokensEligibleForRewards > 0, "ZapRewards: User has no eligable rewards");Ultimately, by staking zero amounts, a user is able to DoS the accrual of standard pool and bonus pool rewards for vault holders.
Recommendation
Validate that
amountToStakeis greater than zero for vaults and greater than a minimum amount for tokens. -
H-01 High Unstake Fee Bypassed Logical Error Resolved
Description
Users are supposed to be charged a penalty if they unstake early prior to expiration. However, a user can simply stake with
autoRelockand always avoid the penalty logic, causing no disincentive to unstake early and a loss of fees for the protocol.Recommendation
Remove the direct check for
autoRelockfrom theif-statementthat determines if a user has to pay a fee for unstaking early. A potential solution may be:timeNow < stake.expiresAt || (stake.autoRelock && !isWithinGracePeriod) -
H-02 High
endIndexIs Incorrect When The Chonk Is Final Logical Error ResolvedDescription
The
endIndexis equal toamountOfLoopIterationswhen it is final chonk. However, this is incorrect becauseamountOfLoopIterationsis not the end index; it represents the difference between the start index and the end index.For example:
startingIndexOfChonk: 100,sizeOfChonk: 20,totalUsersWithStake: 110As a result of this,
isFinalwill be true,amountOfLoopIterationswill be 10, andendIndexwill also be 10. The final chonk will not be iterated at all since the loopfor (uint32 i = startingIndexOfChonk; i < endIndex; i++)will not be executed.Recommendation
The
endIndexshould beamountOfLoopIterations + startingIndexOfChonkregardless of whetherisFinalis true or not, sinceamountOfLoopIterationsis already updated in the_prepareChonkfunction based on the total number of stakers. -
H-03 High
previousStakeNot Updated For Unstaking Tokens Logical Error ResolvedDescription
The two most important parameters in the
TokenStakestruct aretokensStakedandpreviousStake. Ideally, these should be updated whenever a new stake or unstake action is performed. However, thepreviousStakevariable is not updated in theunstakeTokensfunction.Since this state variable is used when calculating rewards in the
ZapRewardscontract, incorrect state updates will lead to both user-specific rewards and the total reward weight being calculated incorrectly, leading to loss of rewards for users.Recommendation
Update the
stake.previousStakestate variable within theunstakeTokensfunction. -
H-04 High Underflow In
_secondsToDaysLogical Error ResolvedDescription
ZAP offers an
autoRelockfeature which allows a user to keep earning rewards post-expiration of their stake. Within_calculateEligibleStandardPoolTokensForUser, the_checkIfShouldProcessStakeForRewardinternal function will determine that because the stake isautoRelock, it is a valid stake for reward processing even if the expiry is passed.Although the stake is valid and processing is attempted, an underflow will occur in
uint256 daysRemainingInStake = _secondsToDays(vaultStake.expiresAt - timeNow);because the expiration has been passed, so expiresAt < timeNow. Ultimately, this user (and chunk) will be unable to have vault rewards calculated, and the relock feature does not work as intended.Recommendation
Add separate handling for auto-relocking stakes such as an automatic extension of the expiry so underflow does not occur.
-
H-05 High User Can Influence Daily Vesting Update Logical Error Resolved
Description
In
calculateTotalBonusWeightingForChonk, the total bonus pool weight for all users is calculated and stored. This assumes that whencalculateDailyVestingForChonkis called, that the users' info has not been altered.A malicious user could increase his share by purchasing more vaults or getting more referrals in between this two-step process. He would then earn more rewards at the expense of other users.
Recommendation
Consider locking updates to user info in between this two-step process.
-
H-06 High Lost Rewards When Unstake Logical Error Acknowledged
Description
It is intended that users still receive rewards based on their previous stake when they unstake after the reward distribution has started, and the
_checkIfShouldProcessStakeForRewardfunction is used for this purpose.The
_checkIfShouldProcessStakeForRewardfunction is called within the_calculateEligibleStandardPoolTokensForUsermethod, using data retrieved fromzapStaking.getUserVaultStakes.However, when a vault is unstaked, its
vaultsStakedvalue is set to 0, which causesgetUserVaultStakesto skip those vaults. As a result, only vaults that are still staked will be considered. This means that users who unstake their vaults after the rewards distribution has begun will lose out on rewards, as unstaked vaults are excluded from the reward calculation.Recommendation
Instead of calling
getUserVaultStakesusegetVaultStake -
H-07 High User Can Influence Distribution In ZapRewards Logical Error Resolved
Description
In calculateDailyVaultHolderWeightingsForChonk, the total standard pool weight for all users is calculated and stored. This assumes that when saveCalculatedVaultHolderRewardsToState is called, that the users' info has not been altered.
A malicious user could increase his share by purchasing more vaults in between this two-step process and would then earn more rewards at the expense of other users.
Recommendation
Consider locking updates to user info in between this two-step process.
-
H-08 High Incorrect Forecasted Rewards Logical Error Resolved
Description
The protocols offers users the option to stake their vaults, so they are awarded rewards not just for their current emitted tokens but also on all of their forecasted emissions for the duration of their stake.
The
postHalvingEventEmissionsare calculated by forecasting the rewards over the stake duration past the next halving, using the total daily rewards at next halving event as the daily emission rate. The issue is that some stakes can cross more than a single halving period.Consider the following example:
- The current day is 179, HALVING_EVENT_1_OCCURANCE_DAY is at 180 days, and HALVING_EVENT_2_OCCURANCE_DAY is at 360 days.
- User stakes for 360 days, which will pass HALVING_EVENT_1_OCCURANCE_DAY and HALVING_EVENT_2_OCCURANCE_DAY.
- When calculating the user's
postHalvingEventEmissionsprior to the first halving,tokensVestedAtNextHalvingPeriodwill beHALVING_EVENT_1_DISTRO_PER_DAY postHalvingEventEmissionsare inflated for the user since theHALVING_EVENT_2_DISTRO_PER_DAYis ignored for a portion of their stake duration.- 1 day accounted for the current emission until the first halving event, 180 days accounted for the emissions after the first halving, and the 179 days of vesting after the second halving event ended up using the daily emissions from the first halving event.
Ultimately, the
totalTokensToVestOverStakeis inflated and certain users will earn more rewards at the expense of others. Also note that thedailyTokenEmissionPerVaultin the calculation is assumed to be consistent across the days staked, but the daily token emission can change from day-to-day as the proportion of vaults a user holds relative to total vaults sold changes.Recommendation
Consider adding logic to account for multiple halvings being passed. Furthermore, document the assumption about the daily emission rate staying constant for the forecast.
-
H-09 High
existingLockedTokensForStakeWrong Denominator Logical Error ResolvedDescription
In ZapRewards, the
_calculateEligibleStandardPoolTokensForUserfunction calculates locked rewards that have already vested out as:uint256 existingLockedTokensForStake = (totalUserLockedTokens * vaultsStaked) / totalVaultsHeldByUserHowever,
totalVaultsHeldByUserincludes claimed vaults when it should only be using unclaimed vaults.Consider this example: Alice has claimed 2 out of 5 vaults and vested 100 tokens for the day tokensUnlocked: 2/5 * 100 = 40 tokensLocked: 3/5 * 100 = 60
If she has staked all remaining 3 vaults:
existingLockedTokensForStake: 3/5 * 60 = 36This implies that she never gets the full rewards for her locked tokens despite staking all remaining vaults. Instead, she should benefit from the full locked amount.
Recommendation
Change the calculation to:
(totalUserLockedTokens * vaultsStaked) / (totalVaultsHeldByUser - userState.vaultsClaimed) -
H-10 High Not earning rewards after vesting ends DOS Resolved
Description
After vesting ends, stakers should still receive rewards in
ZapRewards. This should be done by first callingcalculateDailyVaultHolderWeightingsForChonkin order to_resetState. However, that function will invokeVaultVesting.getDaysLeftOfVesting()which will revert becausedaysCalculated > TOTAL_DAYS_VESTING.Recommendation
Return 0 from
getDaysLeftOfVestingifdaysCalculated > TOTAL_DAYS_VESTING. -
H-11 High Continue over require in save DOS Resolved
Description
When rewards are saved for vaults, the transaction will revert if
standardPoolTokensEligibleForRewardsis 0 because of the following require statement. A user may (maliciously or not) cause a DOS with this revert if all of their stakes have expired. The getVaultStakerById will return the address of the staker even if the stake is expired. Then_checkIfShouldProcessReward()will returnfalseandstandardPoolTokensEligibleForRewardswill end up being 0 causing the whole transaction to revert.Another possible case when this may happen is if after the vesting period ended a user with no locked tokens stakes a new vault.
Recommendation
Consider doing a
continueinstead ofrequirerevert. -
M-01 Medium Missing Recovery Functions Logical Error Resolved
Description
A large amount of ZapTokens are expected to be transferred to the VaultVesting and ZapRewards contracts for users to claim.
After the entire vesting duration, it is likely that there will be leftover tokens unclaimed. Furthermore, there may be some leftover tokens due to rounding over time. These tokens should be recoverable by admin.
Recommendation
Consider allowing ZAP tokens to be covered by privileged address post-vesting period.
-
M-02 Medium Less rewards for expired stakes Logical Error Acknowledged
Description
If a user unstakes after the
rewardsDistroStartTimestamp, they should still get staking rewards. However, if the stake has expired after that timestamp, the user will lose their rewards.This is because
_checkIfShouldProcessStakeForRewardwill mark the stake as invalid during the calculations. This is asymmetrical behavior that may lead to unexpected loss of rewards for some users.Recommendation
Consider checking if the stake has expired after the start distribution timestamp and if so, reward them accordingly.
- if (!autoRelock && expiresAt < block.timestamp) return (false, amountStaked); + if (!autoRelock && expiresAt < rewardsDistroStartTimestamp) return (false, amountStaked); -
M-03 Medium Off by One Error In
vestingPeriodHasNotElapsedOff By One ResolvedDescription
vestingPeriodHasNotElapsedmodifier will pass when thedaysCalculatedis 900. However, it should revert when calculated days reaches to 900.Recommendation
Change the modifier to
require(TOTAL_VESTING_DAYS > daysCalculated) -
M-04 Medium Restaking Can Be Gamed For Higher Rewards Logical Error Resolved
Description
While enabling auto-relock may seem like the optimal strategy for maximizing staking rewards, savvy users can achieve better results by manually restaking daily.
By doing this, they ensure their remaining staking days are consistently maximized, allowing them to earn more rewards than users who rely solely on auto-relock.
This behavior is unexpected and may create an unfair advantage for users who manually restake, potentially disadvantaging regular users who choose the auto-relock option.
Recommendation
Consider if this gaming is acceptable to the protocol. If not, a potential solution is to allow restaking only after some time has passed. Alternatively, a redesign of the forecasted rewards mechanism may be required.
-
L-01 Low Inconsistency Between startingIndexOfChonks Warning Resolved
Description
Both VaultVesting and ZapRewards use a variable startingIndexOfChonk when calculating daily weights in calculateTotalBonusWeightingForChonk and calculateDailyVaultHolderWeightingsForChonk respectively.
However, VaultVesting expects startingIndexOfChonk to be 1 -- since the first user index starts from 1 -- while ZapRewards expects it to be 0 since it does address vaultStaker = zapStaking.getVaultStakerByIndex(i + 1); to account for user index also starting from 1.
This discrepancy can lead to admin errors and possibly result in missing the first user when calculating daily weights in ZapRewards.
Recommendation
Be aware of this difference and account for it in offchan processes.
-
L-02 Low Gas Wastage In Loop Gas Optimization Resolved
Description
The loop in
calculateDailyVaultHolderWeightingsForChonkcalls_getCurrentVestingInformationfor each user. However, this information does not change across users and could only be called once.Recommendation
Consider retrieving the vesting information outside of the for-loop.
-
L-03 Low Wrong VaultVesting iterations count Logical Error Acknowledged
Description
Function
calculateTotalBonusWeightingForChonktries sets a limit of how much iterations can be done. If the starting index of the chonk and the chonk size surpass the available accounts, the result is capped. However, the following calculation is wrong:amountOfLoopIterations = (nextAccountID - startingIndexOfChonk) + 2Let's say there are 2 master accounts and 2 normal accounts. If admins start from 0 and pass a very large chonk size, we will be calculating this amount of loop iterations as:
amountOfLoopIterations = (5 - 0) + 2 = 7This will result in looping 7 times and calculating bonus weights for accounts that don't exist, i.e
address(0).Recommendation
Don't add 2 to
amountOfLoopIterations -
L-04 Low Uninitialized Treasury Account In ZapStaking Warning Resolved
Description
The treasury account is not set in
initializebut withinsetTreasuryAddressby an admin.While the treasury address is the zero address, unstake functions would revert when trying to send tokens to the zero address.
Recommendation
Be aware of this and set the treasury address early, potentially in the
initializefunction. -
L-05 Low Unused State Variables Superfluous Code Resolved
Description
nextTokenStakerIdvariable andallTokenStakersmapping in theZapStakingcontract are never used.Recommendation
Consider removing unused state variables.
-
L-06 Low Incorrect Parameter In
UnstakedEvent Events ResolvedDescription
The
Unstakedevent includes anearlyUnstakeFeeparameter which is hardcoded to 0 when emitting events, regardless of whether the fee was actually 0 or not.Recommendation
It is recommended to emit the actual early unstake fee during events.
-
L-07 Low Forecast Doubelcounting Current Day Warning Acknowledged
Description
The total tokens over the lifetime of the vault stake is calculated with uint256 totalTokensOverLifetimeOfStake = totalTokensToVestOverStake + existingLockedTokensForStake;
If the calculation was done at the exact time such that vaultStake.expiresAt - timeNow is a perfect multiple of days, it is possible for totalTokensToVestOverStake to include the current day as remaining.
This can be problematic since existingLockedTokensForStake already included the rewards for the current day, which can lead to double counting and inflated standard pool rewards.
Recommendation
This is an unlikely scenario, so just be aware of when calculations are performed.
-
L-08 Low Updating Wrong Variable For Token Staker Rewards Warning Resolved
Description
In
calculateTotalTokenStakerRewardsForChonk, the loop updates the state variabletotalStandardPoolWeightinginstead of the local variabletotalStandardPoolTokens.This results in gas wastage and defeats the purpose of the local variable.
Recommendation
Increment the
totalStandardPoolTokensvariable inside the loop. -
L-09 Low Vault staking incentives Warning Acknowledged
Description
If a user got 0 locked tokens and acquires a new vault that they stake it after the vesting has ended they will receive no rewards.
Recommendation
Warn users they have no benefit of staking in these conditions.
-
L-10 Low Multiply Before Dividing Precision Resolved
Description
In
_calculateTokensWithNegativeMultiplier, tokens are divided by100before multiplying bynegativeMultiplier.This leads to loss of precision due to rounding and users therefore lose tokens. For example, even if negativeMultiplier was
100, the user will still experience some loss due to rounding.Recommendation
Perform multiplication before the division.
-
L-11 Low Daily function may be executed more Validation Acknowledged
Description
There are numerous daily functions which have to be called every day in order to calculate state variables used for rewarding both in
VaultVestingandZapRewards. Currently, these function can be called more than once per day. If that happens, the state variables used for calculating rewards will be incorrect and users will receive wrong rewards.Recommendation
Be sure to call these function exactly once per day.
-
L-12 Low Incorrect Tokens Emitted On Certain Days Logical Error Resolved
Description
In
calculateDailyVestingForChonk,daysCalculatedis incremented before calculatingtokensEmitted. This inflatestokensEmittedif daysCalculated crosses into a new halving event.Recommendation
Calculate
tokensEmittedfirst before incrementingdaysCalculated. -
L-13 Low preTGE ratio is the same for all users Warning Resolved
Description
Everyday stakers receive rewards from the
ZapRewardsstandard pool based on their(userWeight / totalWeight) proportion. There are also bonus pool rewards given to vault stakers and these rewards should be calculated using the value obtained by the user's preTGE tokens multiplied by a similar ratio.//In saveCalculatedVaultHolderRewardsToState standardPoolRewards = (standardPoolTokensEligibleForRewards * DAILY_STANDARD_POOL_TOKENS) / totalStandardPoolWeighting // In _calculateBonusPoolTokensForVaultHolder bonusTokens = standardPoolRewards * preTGE / standardPoolTokensEligibleForRewards=> bonusTokens = (standardPoolTokensEligibleForRewards * DAILY_STANDARD_POOL_TOKENS / totalStandardPoolWeighting) * preTGE / standardPoolTokensEligibleForRewards=> bonusTokens = (standardPoolTokensEligibleForRewards * DAILY_STANDARD_POOL_TOKENS * preTGE) / (totalStandardPoolWeighting * standardTokensEligibleForRewards) => bonusTokens = (DAILY_STANDARD_POOL_TOKENS * preTGE) / totalStandardPoolWeightingAfter unraveling the bonus pool tokens earned, the final proportion used to adjust the staked pre-TGE tokens is the
(DAILY_STANDARD_POOL_TOKENS / totalStandardPoolWeighting). The amount of tokens in the standard pool is hardcoded to12950e18and thetotalStandardPoolWeightingis shared across all users in the calculation. Consequently, the only unique portion of the bonus pool rewards for each user is the amount of pre-TGE tokens they hold per staked vault. This may be unexpected as the pre-TGE tokens are not directly correlated with the standard pool weighting, which is contrast to the standard pool reward calculation.Recommendation
Ensure this is the intended protocol tokenomics for bonus pool rewards.
-
L-14 Low Potentially Unexpected Returning Staker Calculation Logical Error Resolved
Description
In functions
isReturningVaultStakerandisReturningTokenStakertheuserStakeCountis incremented regardless of the status of the user’s stake. Because it will always be incremented to three since there are three tiers, a vault staker will only be considered returning if a user had previously unstaked from all three tiers which may be unexpected functionality.Recommendation
Clarify the logic behind returning stakers or just set
userStakeCount = 3if the function is meant to check whether a user unstaked from all three vaults previously. -
L-15 Low Import Can Be Finished Multiple Times Validation Resolved
Description
The default admin is able to call function
finishImportmultiple times, which can be used to changedaysCalculatedto various values and negatively influence what halving the protocol is currently in and how many rewards are distributed.Recommendation
Consider restricting how many times
finishImportcan be called or clearly documenting this behavior. -
H-12 High Early Unstake Fee Calculation Could Underflow DOS Resolved
Description
When users unstake early
calculateEarlyUnstakeFeeapplies a fee % they have to pay based on how much of their stake duration has passed. This function doesn't consider stakes withautoRelock = true.Because of that, after a stake relocks,
percentageProgressThroughStakewill exceed 100%. In result:- For as long as this percentage is less than 100% of the 75% of the stake duration, autoRelock users will be able to unlock earlier by paying much smaller fee.
- Once the percentage goes above 100% of these 75% of the stake duration, the following calculation will revert and autoRelocks will not be able to unstake:
(100 * PCT_BASE) - percentageProgressThroughStakeRecommendation
Add a modulus operator in
calculateEarlyUnstakeFeeto handle autoRelocks
Remediation Review
8 findings · October 10, 2024-
C-01 Critical Underflow
_createNewExpiryTimeForAutoRelock()AcknowledgedDescription
_createNewExpiryTimeForAutoRelock()checks ifexpiresAtis less thantimeNow, and returns early if that is the case. Otherwise it will attempt to storetimeNowminusexpiresAtinto auint256. For this line of code to be hit,expiresAtmust be greater than or equal totimeNow, which will cause an underflow. This will prevent users withautoRelockenabled from restaking early.Recommendation
Change the if statement to check if
expiresAtis larger thantimeNow. -
H-01 High Wrong
standardPoolNegativeMultiplierApplied AcknowledgedDescription
Similarly to
C-04from the main review,_calculateTotalTokenStakerTokensuses the wrong tier for calculations. However, this time it is using the wrongstandardPoolNegativeMultiplierwhen making the calculation foruserTokenWeighting.This occurs because
_calculateTotalTokenStakerTokens()loops throughtokenStakesand usesito get the multiplier fromstakingTiers. If a user only stakes in the third tier,iwill be 0 and it will use the negative multiplier from the first tier.Recommendation
Loop through each tier and if the user does not have any tokens staked in that tier then
continue. -
M-01 Medium Wrong halfway time calculation Acknowledged
Description
ZapStaking.restakeVaults()should allow restaking of vaults only if half of the stake's duration has passed. However, theexpiresAtvariable it passes to the_isLessThanHalfWayThroughStake()function is the time the stake would expire after the restake. In result, the duration of the stake will be greater than it should be and users won't be able to restake their vaults if they haven't passed halfway that duration.Recommendation
Instead of
expiresAt, passvaultStakes[msg.sender][tierIndex].expiresAtto_isLessThanHalfWayThroughStake() -
M-02 Medium Halfway calculations ignore autoRelock Acknowledged
Description
The
_isLessThanHalfWayThroughStake()function will return false iftimeNow > expiresAt. This is fine for normal stakes, but ones withautoRelock = truewill not yield correct result. Once after the first relock happens, the above condition will always be met and users will be able to restake vaults whenever they want to.Recommendation
Consider adding
autoRelocklogic to_isLessThanHalfWayThroughStake(). -
M-03 Medium Wrong autoRelock unstake fee Acknowledged
Description
In order to calculate the fee that needs to be paid when early unstaking an
autorelockstake, theexpiresAtof that stake is adjusted to match the end time of the current (relocked) stake.Then that new
expiresAtis used incalculateEarlyUnstakeFee. However, when such a state relocks, itstakedAtshould also be updated. Because that's not the case, the calculations will be wrong and users will pay less fee because their percentage through the stake will be calculated higher.Let's look at an example:
- Stake on day 0 for 180 days with autoRelock = true
- We are on day 200, which is 20/180 of the stake
- The new
expiresAtwill be 360 - The elapsed time is 200 -
stakedAt= 200 - Duration = 360
- Calculations will return 200 / 360 which is a lot bigger than 20/18
Recommendation
Adjust the
stakedAtforautoRelockas well. -
M-04 Medium recoverTokensAfterVesting off by one Acknowledged
Description
Vesting is completed when
daysCalculatedis equal to or greater thanTOTAL_VESTING_DAYSbut the current recovery mechanism will prevent token recovery whenTOTAL_VESTING_DAYS == daysCalculated:require(TOTAL_VESTING_DAYS < daysCalculated, "Vesting period has not completed");Recommendation
Modify
<to<=inrecoverTokensAfterVesting -
L-01 Low Malicious token recovery Acknowledged
Description
The newly added
VaultVesting.recoverTokensAfterVesting()function allows the admin of the contract to recover any tokens sent to the contract. The intention of that function is to retrieve the difference between the originally sent tokens and the actually distributed amount. However, this function can be used by the admin to transfer locked/unlocked tokens of eligible users out of the contract.Recommendation
Be aware of the risk.
-
L-02 Low Unstaking fees affects rewards Acknowledged
Description
When users unstake their vaults, the fees paid are taken from their locked tokens. This will affect the rewards earned in
ZapRewardsfor the past because locked tokens will have decreased, but the available vaults will not change. Because of that users will receive overall less rewards.Recommendation
Be sure you are aware of this behavior.
No findings match.
More from Zap
Put your code through the same review.
This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.
