Abracadabra engaged Guardian to review the security of its staking rewards contract. From the 29th of January to the 1st of February, a team of 6 auditors reviewed the source code in scope.
- Published
- Review window
- January 29 to February 1, 2024
- Language
- Solidity
- Chains
- Arbitrum
- Sector
- Lending
- 0 Critical
- 1 High
- 3 Medium
- 11 Low
- 0 Informational
Scope
Overview
Abracadabra engaged Guardian to review the security of its staking rewards contract. From the 29th of January to the 1st of February, a team of 6 auditors reviewed the source code in scope.
Findings 15
-
H-01 High Compounding Rewards Dilutes Rewards For Others Gaming Resolved
Description
Proof of concept: PoC
In the rewards system, rewards are not locked and can be claimed at any point in time, even if the user's staking tokens are locked for the 13 week period.
A user with a large portion of the
totalSupplymay continuously claim their rewards and lock those tokens to gain an even greater portion, whether it be directly staking if the reward token matches the staking token, or with a swap to the staking token from the reward token.This will ultimately dilute the rewards for other users in the reward period while massively swaying the rewards towards themselves.
The change in distribution is at the expense of other staked users, as there is a set amount to be distributed and the final rewards are dramatically skewed towards the user due to their continuous compounding while other staked users have their rewards fractionalized.
Recommendation
Consider if this is the expected behavior. If this is expected then clearly document it for users so they have a chance to collect an appropriate amount of rewards. If this is not expected, consider requiring that rewards can only be claimed at the end of the lock period.
Resolution
Abracadabra Team: The issue was resolved in commit 3081d38.
Guardian Team: Because reward periods do not necessarily align with epochs, rewards may still be compounded within a reward period. Consider forcing reward notifications to adhere exactly to epoch time windows, otherwise be aware of this behavior and clearly document it for users.
Abracadabra Team: Reward periods are now tied directly to epochs in commit 78b7cfa.
-
M-01 Medium Potential System DoS DoS Resolved
Description
In the
_updateRewardsfunction therewardTokensare iterated over to_updateRewardsGlobalfor each token. However there is no explicit bound on the length of therewardTokensarray.As a result it is possible for the
rewardTokensarray to become so long that updating the rewards for each token requires more gas than the block gas limit allows.In such a scenario the
_updateRewardsfunction and all functions relying on it would be DoS’d. Similarly in the_getRewardsfunction allrewardTokensare iterated over to pay the user’s rewards.Therefore the owner may DoS the claiming of rewards by adding many
rewardTokenswith theaddRewardfunction.Recommendation
Consider adding a limit to the amount of
rewardTokensthat may be supported in theaddRewardfunction.Resolution
Abracadabra Team: The issue was resolved in commit 0bdf6d5.
-
M-02 Medium Trapped MIM Rewards Trapped Funds Resolved
Description
In the
_rewardPerTokenfunction if the totalSupply is 0 therewardPerTokenStoredis not advanced, meaning that rewards are not accrued until a user stakes.This behavior is fine when a user does stake at some point during the reward period, however in the case that no users stake for an entire reward period, the rewards for that period will not be distributed and will have to be claimed using the recover function.
In the case where MIM is used as a rewardToken, the undistributed rewards will not be recoverable as the stakingToken cannot be recovered. Therefore any MIM rewards that go undistributed will be locked.
Recommendation
If the
totalSupplyis 0 before the end of a reward period be sure to stake a trivial amount in order to capture any undistributed MIM rewards before calling thenotifyRewardAmountfunction.Resolution
Abracadabra Team: The issue was resolved in commit 710f511.
Guardian Team: Upon recovery the reward rate is not updated to reflect the new reward token balance in the staking contract. Consider adjusting the reward rate, otherwise be careful to not remove tokens that are necessary for user rewards.
-
M-03 Medium _createLocks DoS DoS Resolved
Description
In the
processExpiredLocksfunction there is no requirement that the operator must process the oldest expired lock first. Therefore if an operator neglects to process expired locks for a user directly when they unlock it becomes possible for an operator to errantly or maliciously process the lock at thelastLockIndexbefore processing the other locks.Depending on where the
lastLockIndexis relative to the user’s locks array this can yield different outcomes:In the following example lock A expired before lock B and lock B expired before lock C, lock C is the true last lock:
- lastLockIndex is 2
- The user’s locks array is [A, B, C]
- All locks are expired, the operator processes lock C first
- The user’s locks array is now [A, B], the lastLockIndex is still 2
However, 2 is an invalid index for the array and now the user cannot create a new lock as the
_createLockfunction will revert with an array index out of bounds error.Recommendation
Consider requiring that the lock at the
lastLockIndexmay only be processed when there is only 1 item left in the locks array. Additionally, be sure to carefully process the correct locks on time.Resolution
Abracadabra Team: The issue was resolved in commit a312ff3.
-
L-01 Low LogLockIndexChanged Invalid Index Update Events Resolved
Description
In the
processExpiredLocksfunction when the lock being processed is the final lock in the array, theLogLockIndexChangedevent will still emit with afromIndexof the last index and atoIndexof the last index.In this case no lock index changed, therefore the
LogLockIndexChangedevent may be misleading.Recommendation
Consider only emitting the
LogLockIndexChangedevent when theindex != lastIndex.Resolution
Abracadabra Team: The issue was resolved in commit 00cc23a.
-
L-02 Low Lacking rewardToken Validation Validation Resolved
Description
In the
addRewardfunction there is no validation that the rewardToken is not already present in therewardTokenslist. There is no significant issue with a duplicaterewardToken, however it would cause unnecessary gas expenditure upon updating and claiming rewards.Recommendation
Consider using an
EnumerableSetfor therewardTokensand validating that therewardTokenis not already present in the set before adding it in theaddRewardfunction.Resolution
Abracadabra Team: The issue was resolved in commit bcd8d9a.
-
L-03 Low notifyRewardAmount Does Not Validate rewardToken Validation Resolved
Description
The function
notifyRewardAmountis used by operators to update the distribution rate and period of a reward token.The issue is that there is another function
addRewardwhich actually adds the reward token to the rewardTokens array state variable.If the operator uses
notifyRewardAmountto distribute a reward token that does not exist in the array, these rewards will not be accounted for as most reward-related functions will loop through the reward token arrays.Recommendation
Consider adding a check to
notifyRewardAmountto verify that the reward token was already added by the owner.Resolution
Abracadabra Team: The issue was resolved in commit 1c20437.
-
L-04 Low Potentially Invalid maxLocks Value Validation Resolved
Description
In the constructor the maxLocks is assigned to the result of
_lockDuration / _rewardsDuration, this result is valid when the _lockDuration is an exact multiple of the_rewardsDuration, but in any case where the_lockDurationis not an exact multiple of the_rewardsDurationthemaxLocksis 1 less than it should be due to truncation.For example:
- _lockDuration = 5 weeks
- _rewardsDuration = 2 weeks
- maxLocks = 5 / 2 = 2
- However 3 distinct reward periods will intersect the 5 week lock period, therefore a user may have a
maximum of 3 locks.
It is unlikely that the
_lockDurationwould not be a perfect multiple of the_rewardsDuration, however this edge case ought to be either handled or validated against in the constructor.Recommendation
Either validate that
_lockDuration % _rewardsDuration == 0or assignmaxLocksto(_lockDuration +_rewardsDuration • 1) / _rewardsDuration.Resolution
Abracadabra Team: The issue was resolved in commit b84ba2f.
-
L-05 Low Invalid Withdraw Function Documentation Documentation Resolved
Description
The documentation for the withdraw function suggests that the function will “iterate through the locks to find expired locks, prunning them and cumulate the amounts to withdraw”, however the withdraw function does not implement this behavior.
Recommendation
Consider implementing this behavior for the withdraw function or remove the comment on this behavior.
Resolution
Abracadabra Team: The issue was resolved in commit 1d99a3d.
-
L-06 Low Malicious Operator May Reduce Rewards Centralization Resolved
Description
In the
notifyRewardAmountfunction therewardRateis assigned to the result ofamount /rewardsDuration, which will include precision loss up to 604800 when therewardsDurationis 1 week long.This amount of precision loss will be negligible for tokens with 18 decimals, however if a token such as USDC, with 6 decimals, is used as the
rewardTokenthen this amount of precision loss may compound to be nontrivial over time.Additionally, a malicious operator could leverage this precision loss to cause significant loss of assets for users (if a 6 decimal token is used). The operator could donate a 0 amount to the
notifyRewardAmountfunction and cause reward periods to end when they have only just begun in order to update therewardRateand cause precision loss on the total amount to be distributed.The malicious operator could donate a 0 amount in a loop this way to ultimately cause a significant reduction in the
rewardRatedue to precision loss.Recommendation
There is already a recover function to rescue any funds lost due to precision. To address the potential gaming by a malicious operator, consider requiring that a non-trivial amount of the token is donated or that the previous period is fully finished before starting a new one.
Resolution
Abracadabra Team: The issue was resolved in commit bcd8d9a.
-
L-07 Low Rewards Accrue For Expired Locks Unexpected Behavior Acknowledged
Description
The purpose of the
processExpiredLocksfunction is to transfer thelocked BalancetounlockedBalance.The rewards accrued by the user is calculated based on the
balanceOf(user), which is calculated in the following way:bal.unlocked + ((bal.locked * lockingBoostMultiplerInBips) / BIPS).If there is a delay in calling
processExpiredLocksthen the users will still accrue the rewards for their expired locks.Recommendation
Consider if this is the expected behavior, otherwise be sure to process expired locks on time.
Resolution
Abracadabra Team: Acknowledged.
-
L-08 Low Lacking Constructor Validation Validation Resolved
Description
In the constructor there is no validation that the
_lockDurationand_rewardsDurationare not too short nor too long.Additionally there is no validation that the
lockingBoostMultiplieris greater than the minimum basis points divisor, otherwise users would be punished for locking rather than being rewarded.Recommendation
Consider adding validation in the constructor for the
_lockDuration,_rewardsDuration, andlockingBoostMultiplierResolution
Abracadabra Team: The issue was resolved in commit a8f386a.
Guardian Team: The
MIN_BOOST_MULTIPLIERvalidation does not ensure locking provides a boost. Consider reverting if_lockingBoostMultiplierInBips <= BIPSinstead to ensure there is a boost for locking.Abracadabra Team: The validation was updated in commit c34decd.
-
L-09 Low Rewards Are Granted Immediately After Locking Unexpected Behavior Acknowledged
Description
User’s locked balances are updated immediately upon locking, even though technically their locking period does not begin until the end of the week they locked within.
This means that user’s will begin receiving the locking rewards outside of the 13 week locking period, which can reduce the rewards earned by current lockers and may be unexpected by the protocol.
Recommendation
Consider if this is expected behavior, if it is not then only allow users to accrue locking rewards once their 13 week lock period has begun.
Resolution
Abracadabra Team: Acknowledged.
-
L-10 Low Invalid Lock Indexes May Cause Out-of-Bounds Unexpected Behavior Acknowledged
Description
The operator must pass the
lockIndexesin consideration of an element’s placement after the length decrements from the prior indexes.Consider the following scenario:
- Bob has 3 expired locks [0, 1, 2]
- Operator passes
lockIndexes = [0, 1, 2] - After processing indexes 0 and 1, the lock array’s length will only be 1. However, the current
indexto
process is 2.
- Consequently,
processExpiredLocksrevertsif (locks[index].unlockTime > block.timestamp)due to
out-of-bounds.
Recommendation
Consider documenting this scenario and ensure operators are able to track the to-be indexes if needed.
Resolution
Abracadabra Team: We will ensure our gelato task processing locks does this correctly.
-
L-11 Low Sequencer Outage Prevents Important Actions Warning Acknowledged
Description
When the Arbitrum sequencer is down, transactions to the
LockingMultiRewardscontract can still be recorded using theDelayedInbox.This allows users to access the contract but will not allow operators and admin to call the restricted functions through DelayedInbox due to the way address aliasing works.
From the Arbitrum Docs, "when these messages are executed on L2, the sender's address —i.e., that which is returned by msg.sender — will not simply be the L1 address that sent the message; rather it will be the address's "L2 Alias." An address's L2 alias is its value increased by the hex value 0x1111000000000000000000000000000000001111"
L2_Alias = L1_Contract_Address + 0x1111000000000000000000000000000000001111As a result the owner will not be able to pause or unpause the contract when the sequencer is down, and operators will not be able to process locks nor notify rewards.
Recommendation
Be aware of this scenario and have a contingency plan in the event that the sequencer is down for an extended period.
Resolution
Abracadabra Team: Acknowledged.
No findings match.
Invariants 19
The review's fuzzing suite asserted 19 invariants. 19 held.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
LMR-01 | User Locks Do Not Exceed Max Locks | Held |
LMR-02 | Earned Rewards Do Not Exceed Reward Token Balance | Held |
LMR-03 | Sum of Users Locked Balance = Total Locked | Held |
LMR-04 | Sum of Users Unlocked Balance = Total Unlocked | Held |
LMR-05 | Sum of Users Balance = Total Supply | Held |
LMR-06 | Last Reward Time Applicable Is Not Less Than The Reward Period’s Last Update Time | Held |
LMR-07 | Latest Lock Has The Latest Unlock Time | Held |
LMR-08 | Total Supply Accurately Increased After Staking | Held |
LMR-09 | User’s Staking Contract Balance Accurately Increased After Staking | Held |
LMR-10 | User’s Token Balance Decremented By Staked Amount | Held |
LMR-11 | Locking Accurately Increases Total Supply | Held |
LMR-12 | User’s Staking Contract Balance Accurately Increased After Locking | Held |
LMR-13 | User’s Token Balance Unchanged After Locking | Held |
LMR-14 | Withdraw Decreases Unlocked Balance By Withdrawn Amount | Held |
LMR-15 | User’s Staking Contract Balance Decreased By Withdrawn Amount | Held |
LMR-16 | User’s Token Balance Increased By Withdrawn Amount | Held |
LMR-17 | User’s Token Balance Increased By Reward Amount | Held |
LMR-18 | User Rewards After Getting Rewards Is 0 | Held |
LMR-19 | Total Supply Unchanged After Getting Rewards | Held |
More from Abracadabra Money
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.
