Guardian's review of OFT for Abracadabra Money, published December 2024. The report records 48 findings across 2 review rounds, including 4 high and 5 medium.
- Published
- Review window
- November 12 to December 14, 2024
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Ethereum, Arbitrum
- Sector
- Staking, Tokens
- 0 Critical
- 4 High
- 5 Medium
- 39 Low
- 0 Informational
Scope
6 files in scope · 811 nSLOC
| File | nSLOC | Lines |
|---|---|---|
src/mixins/Create3Factory.sol | 15 | 21 |
src/periphery/BoundSpellCrosschainActions.sol | 198 | 284 |
src/periphery/MultiRewardsClaimingHandler.sol | 82 | 116 |
src/periphery/TokenLocker.sol | 231 | 323 |
src/staking/MultiRewards.sol | 241 | 333 |
src/staking/SpellPowerStaking.sol | 44 | 67 |
Findings 48
Main Review
39 findings · November 12 to 18, 2024-
H-01 High stakeFor Function Is Not Overridden Logical Error Resolved
Description
In the
SpellPowerStakingcontract there is a lockup period where users are unable to exit their stakes if they have lastAdded to their stake within thelockupPeriod. However the lockup logic is only enforced for thestakefunction, and not for thestakeForfunction as this is not overridden from theMultiRewardscontract. Therefore users who stake through the cross-chain staking system which uses theBoundSpellActionReceiveras an operator to stake for a user address will not have their stakes locked for thelockupPeriod. Furthermore thewhenNotPausedmodifier is not applied to thestakeForfunction as it is not overridden, so stakes through the cross-chain mechanism can still occur when the system is paused.Recommendation
Override the
stakeForfunction in theSpellPowerStakingcontract and apply thelastAddedstorage write as well as thewhenNotPausedmodifier. -
H-02 High SpellPowerStaking Does Not Override The MultiRewards.stakeFor Function Validation Resolved
Description
The
BoundSpellActionReceivercontract interacts with theSpellPowerStakingcontract via itsstakeForfunction. Specifically, when cross-chain actions are triggered through LayerZero, the_handleMintBoundSpellAndStakeand_handleStakeBoundSpellfunctions callspellPowerStaking.stakeFor. However, theSpellPowerStakingcontract enforces a lockup period for withdrawals via thelastAddedmapping, which tracks the last timestamp at which a user staked tokens. This lockup mechanism is implemented in the stake function ofSpellPowerStaking, however, thestakeForfunction in theMultiRewardscontract (from whichSpellPowerStakinginherits) is not overridden inSpellPowerStaking. As a result, when staking occurs through theBoundSpellActionReceiverusingstakeFor, thelastAddedmapping is not updated. This allows users to bypass the lockup mechanism as they can stake without thelastAddedmapping being updated.Recommendation
Override the
stakeForfunction in theSpellPowerStakingcontract as shown below:function stakeFor(address user, uint256 amount) public virtual override { super.stakeFor(user, amount); lastAdded[user] = block.timestamp; } -
H-03 High Lockup period can be avoided Protocol Resolved
Description
There is a
lockupPeriodthat must pass after a stake before the user is able to withdraw in theSpellPowerStakingcontract. ThelastAddedmapping is updated on each stake, allowing the code to check whether the lockup period has passed on a withdrawal trial. However,stakeFordoesn't update that mapping. Any user can use theBoundSpellActionSendercontract to send a stake message and make theBoundSpellActionReceivercall thestakeForfunction. By doing so, they will successfully stake and avoid the lockup period.Recommendation
Update the lockup period on
stakeFor()as well. -
M-01 Medium Potential Revert On Zero Transfer ERC20 Resolved
Description
The
MultiRewards._getRewardsFor()function doesn't check if the current reward of the user for a given token is positive. This means a 0 transfer will be initiated for anyrewardTokenthat hasn't accrued. If one of these tokens revert on 0 amount transfers, the given user will not be able to claim any rewards using that function until they accrue these rewards.Recommendation
Consider executing a
continueif the accrued rewards are 0 in both_getRewardsForandMultiRewardsClaimingHandler.notifyRewards() -
M-02 Medium
getRewardsForreceiver is hardcoded Crosschain ResolvedDescription
When a user calls
_getRewardsFor()withRewardHandlerParams, their rewards will be sent to theMultiRewardsClaimingHandlercontract which will send them to the desired destination chain. The receiver there will be the same address as the sender on the source chain. If the user doesn't have the same address on the other chain - because it's a smart contract or the other chain is not an EVM one, they will lose access to their rewards.Recommendation
Consider letting the user pass a receiver address for the destination chain
-
M-03 Medium Paused staking is possible Pause Resolved
Description
The
MultiRewardscontract can be paused by its owner. When it's paused, no stakes are allowed. However, users may bypass that restriction by initiating a cross chain stake. TheBoundSpellCrosschainActionwill call thestakeForfunction which doesn't check if the contract is paused or not. This will allow users to stake even in a paused state and potentially benefit from the current situation.Recommendation
Consider disallowing
stakeForwhen the contract is paused. -
L-01 Low Funds Held Hostage With Lockup Period Configuration Acknowledged
Description
The
setLockupPeriodfunction can be used to extend thelockupPeriodwithout bound by the owner. This can be used to trap and hold hostage the funds of users who are deposited in the SpellPowerStaking contract. This may become a risk in the event that the owner address is compromised. Instead the configured lockup period could be limited to reduce this risk.Recommendation
Consider adding an upper bound for the lockup period in the
setLockupPeriodfunction. -
L-02 Low Potential Revert On Zero Transfer Configuration Resolved
Description
The
MultiRewards._getRewardsFor()function doesn't check if the current reward of the user for a given token is positive. This means a 0 transfer will be initiated for anyrewardTokenthat hasn't accrued. If one of these tokens revert on 0 amount transfers, the given user will not be able to claim any rewards using that function until they accrue these rewards.Recommendation
Consider executing a
continueif the accrued rewards are 0 in both_getRewardsForandMultiRewardsClaimingHandler.notifyRewards() -
L-03 Low Centralization Risks Centralization Acknowledged
Description
The contracts
TokenLocker,MultiRewards, andSpellPowerStakingcontain certain functions and permissions that could lead to centralization risks. These risks arise from the ability of privileged roles (such as the owner or operators) to perform actions that could potentially be detrimental to the interests of regular users. In theMultiRewardscontract:- Owner can never recover the
stakingTokenhowever he can recover anyrewardToken. getRewardsForfunction could be abused by an operator to send the accrued rewards to himself. In theSpellPowerStakingcontract:- Owner can extend the
lockupPeriodindefinitely. In theTokenLockercontract: - Operators have total control over the tokens within the contract. They are free to
redeemFor,instantRedeemForandclaimForon behalf of any user.
Recommendation
These centralization risks are noted to provide a clear understanding of the control dynamics within the contracts. No specific recommendations are provided as the intention is to document these aspects for awareness and consideration by stakeholders.
- Owner can never recover the
-
L-04 Low MultiRewards Contract Only Supports 18 Decimal Tokens Logical Error Resolved
Description
The
MultiRewardscontract assumes that all reward tokens have 18 decimals, as indicated by the use of1e18in calculations for rewards:function rewardPerToken(address rewardToken) public view returns (uint256) { if (totalSupply == 0) { return _rewardData[rewardToken].rewardPerTokenStored; } uint256 timeElapsed = lastTimeRewardApplicable(rewardToken) - _rewardData[rewardToken].lastUpdateTime; uint256 pendingRewardsPerToken = (timeElapsed * _rewardData[rewardToken].rewardRate * 1e18) / totalSupply; return _rewardData[rewardToken].rewardPerTokenStored + pendingRewardsPerToken; }This assumption will lead to incorrect reward calculations if tokens with different decimal places are used.
Recommendation
Consider adding a check in the
addRewardfunction that enforces that therewardTokenadded has 18 decimals. -
L-05 Low Incompatibility With Tokens That Revert On Zero Value Transfers Logical Error Resolved
Description
Several functions in the contracts do not check for zero value transfers before attempting a token transfer. This can lead to failed transactions if a token reverts on zero value transfers which is a behavior exhibited by some custom ERC20 tokens. The functions affected are:
MultiRewardsClaimingHandler.notifyRewards()MultiRewards._getRewardsFor(address user, RewardHandlerParams memory params)TokenLocker._instantRedeemFor()Moreover, in theMultiRewardsClaimingHandler.notifyRewardsfunction, when there are no pending rewards for a specificrewardToken, the LayerZero'ssendFrommethod is still invoked to create a cross-chain message. This should not be done when the reward amount is zero.
Recommendation
Before any token transfer, add a check to ensure that the amount transferred is greater than zero. On the other hand, ensure that
sendFromis only called when the rewardsamountis higher than zero. -
L-06 Low BoundSpellActionSender.send Allows Sending a Zero Amount Code Best Practices Resolved
Description
The
sendfunction in theBoundSpellActionSendercontract allows users to initiate cross-chain actions with a zero amount. This can lead to unnecessary transactions that consume gas without performing any meaningful operation.Recommendation
Add a require check in the
BoundSpellActionSender.sendfunction that enforces that theamountsent is higher than 0. -
L-07 Low lockingDeadline Parameter Can Be Removed Code Best Practices Acknowledged
Description
In the
TokenLockercontract thelockingDeadlineparameter is passed to theredeemand_redeemForfunctions but it does not seem to influence any logic within these functions. The parameter is not used to enforce any constraints, which makes it redundant in its current form. The only check performed is within the_createLockfunction which simply enforces that the providedlockingDeadlineis higher or equal to the currentblock.timestamp.Recommendation
Consider removing the
lockingDeadlineparameter from theTokenLockercontract. -
L-08 Low Missing Require Check in _updateInstantRedeemParams Function Configuration Resolved
Description
The
_updateInstantRedeemParamsfunction in theTokenLockercontract updates the parameters for instant redemption, including thefeeCollectoraddress. However, there is no require check to ensure that thefeeCollectoraddress is not set toaddress(0). If thefeeCollectoris set toaddress(0), it can cause a denial of service in the insta-redemption process, as theinstantRedeemfunction checks for a non-zerofeeCollectoraddress before proceeding.Recommendation
Consider adding a require check in the
_updateInstantRedeemParamsfunction that enforces that thefeeCollectoraddress set is not theaddress(0). -
L-09 Low TokenLocker._redeemFor Function Claims to the to Address Before Redeeming Validation Acknowledged
Description
In the
TokenLockercontract, the_redeemForfunction is responsible for redeeming a specified amount of tokens. However, before creating a new lock for the redeemed amount, the function calls_claim, which releases any accrued tokens to thetoaddress. This behavior may not be expected by users who intend to redeem a specific amount to a particular address without affecting any accrued tokens.Recommendation
Consider documenting the behavior of the
_redeemForfunction, informing users that it will also claim any accrued tokens to thetoaddress. This will help users understand the full implications of using the function. -
L-10 Low Use of an Outdated Version Of solady’s safeTransferLib Code Best Practices Resolved
Description
Multiple contracts within the system rely on Solady's
SafeTransferLiblibrary for token transfer operations. The version in use, as specified in thesoldeer.lockfile, is:[[dependencies]] name = "solady" version = "0.0.231" source = "https://github.com/Vectorized/solady.git" checksum = "2907b5036b9b3489891e69b49adba24c793940d4"This version of Solady is outdated. In a more recent update, introduced via PR#1128, an important security improvement was added to
SafeTransferLib. The updated version includes a validation step to ensure that the token being transferred is a valid contract address with actual code. By using an outdated Solady’s version, the contracts miss out on this important safeguard. Upgrading to the latest version of Solady is recommended to enhance the security of any token transfer.Recommendation
Consider using the latest Solady’s version.
-
L-11 Low Floating Pragma Code Best Practices Acknowledged
Description
Contracts should be deployed with the same compiler version and flags used during development and testing. Locking the pragma helps to ensure that contracts do not accidentally get deployed using another pragma. For example, an outdated pragma version might introduce bugs that affect the protocol negatively. All the contracts in scope are using the following floating pragma:
pragma solidity >=0.8.0;Recommendation
Consider locking the pragma version in all the smart contracts. It is not recommended to use a floating pragma in production. For example:
pragma solidity 0.8.20. -
L-12 Low Unused Imports Code Best Practices Resolved
Description
In the
TokenLockercontract, theMathLiblibrary is imported but not utilized anywhere within the contract.Recommendation
Consider removing the import statement for the
MathLiblibrary in theTokenLockercontract. -
L-13 Low Lack of a Double Step TransferOwnership Pattern Code Best Practices Acknowledged
Description
The current ownership transfer process for all the contracts inheriting from the
OwnableOperatorscontract involves the current owner calling thetransferOwnershipfunction:function transferOwnership(address newOwner) external onlyOwner { owner = newOwner; emit OwnershipTransferred(msg.sender, newOwner); }If the nominated EOA account is not a valid account, it is entirely possible that the owner may accidentally transfer ownership to an uncontrolled account, losing the access to all functions with the
onlyOwnermodifier.Recommendation
It is recommended to implement a two-step process transfer ownership process where the owner nominates an account and the nominated account needs to call an
acceptOwnershipfunction for the transfer of the ownership to fully succeed. This ensures the nominated EOA account is a valid and active account. This can be easily achieved by using OpenZeppelin’s Ownable2Step contract. -
L-14 Low Incompatibility With Fee-On-Transfer Tokens DoS Acknowledged
Description
All the contracts within scope, upon a token transfer, always assume that the full amount specified will be transferred to the recipient. However, fee-on-transfer tokens deduct a fee from the transfer amount, resulting in the recipient receiving less than expected. For example, tokens like USDT, while currently not implementing transfer fees, have the capability to do so. If a reward token with fees is ever added to the system, it could cause a total denial of service for the reward distribution process, as the expected and actual transferred amounts would not match.
Recommendation
Ensure that the contracts do not operate with fee-on-transfer tokens. In order to support this type of tokens, consider checking the balance before and after the token transfer to determine the total amount transferred after the fees were applied.
-
L-15 Low Unclaimed Dust in MultiRewards Contract Code Best Practices Acknowledged
Description
The
MultiRewardscontract may experience a minor accumulation of unclaimed reward tokens (dust) due to the inherent precision loss in Solidity's integer arithmetic. When calculatingrewardPerTokenand users' earned rewards, minimal precision loss occurs because of integer division truncating fractional values. This precision loss results in a small amount of rewards not being distributed, thus remaining in the contract. Over time, these minimal unclaimed amounts can accumulate within the contract, remaining inaccessible to users.Recommendation
Consider utilizing the
recoverfunction to retrieve any accumulated dust if it becomes significant for any of the reward tokens. -
L-16 Low Unused Error Code Best Practices Resolved
Description
ErrInvalidRewardHandlererror in theMultiRewardscontract is defined but never used.Recommendation
Consider removing the error, or alternatively perform an input check during the
setRewardHandlerfunction and use the defined error. -
L-17 Low Hardcoded LZ Params Configuration Acknowledged
Description
As recommended by the LayerZero integration checklist point 5 & 6 (https://docs.layerzero.network/v1/developers/evm/evm-guides/advanced/relayer-adapter-parameters) the param for
zroPaymentAddressanduseZroshould not be hardcoded. This prevents the ability of using the ZRO token as a fee payment option in the future.Recommendation
Consider passing in an input parameter for the
zroPaymentAddressanduseZrofield. -
L-18 Low Late Redeem Is Incentivized Configuration Acknowledged
Description
When users redeem, they burn their issued tokens and wait a certain amount of time to receive back the underlying token. They must wait for the current epoch to end plus the
lockupPeriod. This means that the later they redeem their tokens in the given epoch, the less they will have to wait compared to users who redeemed earlier in the same epoch.Recommendation
Consider documenting this behavior for users.
-
L-19 Low Unnecessary Parameter Data Code Best Practices Resolved
Description
In the
MINT_AND_STAKE_BOUNDSPELLaction flow theMintBoundSpellAndStakeParamswhich is used includes arewardHandlerParamsentry where thedataandvaluevalues are always hardcoded to""and0. TheserewardHandlerParamsvalues are never used during the execution of the mint and stake and therefore can be removed.Recommendation
Consider removing the
rewardHandlerParamsfrom theMintBoundSpellAndStakeParams. -
L-20 Low Lacking Constant Usage Code Best Practices Resolved
Description
In the
estimatefunction the packet type provided to theminDstGasLookupfunction is hardcoded as the literal 1.Recommendation
Consider using the
PT_SEND_AND_CALLconstant value to follow best practices and avoid magic numbers. -
L-21 Low DOS if custom params are disabled DoS Acknowledged
Description
MultiRewardsClaimingHandler.notifyRewards()populates theadapterParamsfield and then sends a message by callingsendFrom(). ThesendFromfunction will revert if the OFT'suseCustomAdapterParamsvariable is set to false and users won't be able to claim their rewards to another chains.Recommendation
Be sure to never toggle the
useCustomAdapterParams, as it's currently set to true. -
L-22 Low TokenLocker._redeemFor Function Claims to the to Address Before Redeeming Configuration Acknowledged
Description
In the
TokenLockercontract, the_redeemForfunction is responsible for redeeming a specified amount of tokens. However, before creating a new lock for the redeemed amount, the function calls_claim, which releases any accrued tokens to thetoaddress. This behavior may not be expected by users who intend to redeem a specific amount to a particular address without affecting any accrued tokens.Recommendation
Consider documenting the behavior of the
_redeemForfunction, informing users that it will also claim any accrued tokens to thetoaddress. This will help users understand the full implications of using the function. -
L-23 Low Uncleared
lastLockIndexLogical Error ResolvedDescription
In the
TokenLockercontract,_releaseLocksfunction iterates through all the locks, releases the unlocked ones and updates thelastLockIndex. However, if all locks are released at the same time (e.g. a user waits long enough to claim all at once), this line will never hit, thelastLockIndexwill not be updated and it will retain the previous lock count, even though the user no longer has any locks left.Recommendation
Consider resetting
lastLockIndexto 0 if the_userLocks[user].lengthbecomes 0 after releasing locks. -
L-24 Low Layerzero dust issues Configuration Acknowledged
Description
When
MultiRewardsClaimingHandlersends token to the destination chain, layerzero will not use all the tokens, but leave a dust amount of tokens in the contract, depending on the configured OFT decimals. Since there is no refund mechanism once the tokens enter theMultiRewardsClaimingHandler, users will always be losing these dust amounts. The same problem exists inBoundSpellCrosschainActions._sendMintAndStakeBoundSpellandBoundSpellCrosschainActions._sendStakeBoundSpellRecommendation
Consider implementing a mapping with dustAmounts that the users can claim.
-
L-25 Low Ether May Be Stuck In Contract Logical Error Resolved
Description
In
_getRewardsFor, the rewardHandler contract is called as follows:rewardHandler.notifyRewards{value: params.value}(user, _rewards, params.data)However,params.valueis user-defined and may differ frommsg.value, leading to potential excess ETH left in the contract that cannot be retrieved. Furthermore, innotifyRewardsifmsg.value > param.fee + param.gasthen excess ether is also stuck in contract.Recommendation
Consider passing in
msg.valueinstead ofparams.value:rewardHandler.notifyRewards{value: msg.value} -
L-26 Low Can't send native token with CREATE3 Configuration Resolved
Description
The
Create3Factory.deploy()function accepts avalueparameter to use when deploying the new contract. However, there are nopayable,receiveorfallbackfunctions in theCreate3Factorycontract which makes it impossible to send native token for deployment. Technically, it's still possible to send native token to the contract outside of the EVM, but anyone will be able to frontrun the deployment transaction and steal it by deploying their own contract and passing the balance of theCreate3Deployerasvalue. In result, deployments which require sending funds will always fail.Recommendation
Make the
deployfunctionpayable. -
L-27 Low Unclear Process for Handling Failed Messages on Destination Chain Configuration Acknowledged
Description
In the
MultiRewardsClaimingHandlerandBoundSpellCrosschainActionscontracts, LayerZero/OFT calls are used to enable cross-chain operations. However, the procedure for managing scenarios where these messages fail to execute on the destination chain is not well-documented. This can result in some uncertainty regarding who is responsible for retrying or addressing failed transactions, whether it should be handled by a backend system or individually by the users.Recommendation
Provide comprehensive documentation outlining the process for handling failed messages on the destination chain. Ensure that failed messages occur only under temporary circumstances, such as when the destination contract is paused. Permanent failed messages should never occur.
-
L-28 Low Refund Address Should Be Configurable Logical Error Resolved
Description
In
notifyRewards, the user receiving rewards is also set as therefundAddress. However, if the user is a contract without areceivefunction then the refund mechanism will fail and prevent rewards from being claimed. Furthermore, ifnotifyRewardswas called directly by an Operator (who pays the gas and fees), then the Operator should receive the refund instead of the user.Recommendation
Consider allowing a function input for
refundAddressinstead of assuming it should beuser. -
L-29 Low Reward tokens are not removable Configuration Acknowledged
Description
The
rewardTokensadded to theMultiRewardscontract can only be added and not removed. This is dangerous because:- the array may become too large and DOS the system.
- one of the tokens may start reverting causing DOS of the system
Recommendation
Be aware of the risk.
-
L-30 Low
getRewardsForreceiver is hardcoded Configuration ResolvedDescription
When a user calls
_getRewardsFor()withRewardHandlerParams, their rewards will be sent to theMultiRewardsClaimingHandlercontract which will send them to the desired destination chain. The receiver there will be the same address as the sender on the source chain. If the user doesn't have the same address on the other chain - because it's a smart contract or the other chain is not an EVM one, they will lose access to their rewards.Recommendation
Consider letting the user pass a receiver address for the destination chain
-
L-31 Low MultiRewards Contract Only Supports 18 Decimal Tokens Logical Error Resolved
Description
The
MultiRewardscontract assumes that all staking tokens have 18 decimals, as indicated by the use of1e18in calculations for rewards:function rewardPerToken(address rewardToken) public view returns (uint256) { if (totalSupply == 0) { return _rewardData[rewardToken].rewardPerTokenStored; } uint256 timeElapsed = lastTimeRewardApplicable(rewardToken) - _rewardData[rewardToken].lastUpdateTime; uint256 pendingRewardsPerToken = (timeElapsed * _rewardData[rewardToken].rewardRate * 1e18) / totalSupply; return _rewardData[rewardToken].rewardPerTokenStored + pendingRewardsPerToken; }This assumption will lead to incorrect reward calculations if tokens with different decimal places are used.
Recommendation
Consider adding a check in the
addRewardfunction that enforces that the staking token used has 18 decimals. -
L-32 Low Warning About
minDstGasLookupCode Best Practices AcknowledgedDescription
In the
notifyRewardsfunction ofMultiRewardsClaimingHandlercontract, user providedparams.gasamount is passed asgasLimitfor the execution in the destination chain. This amount is then later checked in theLzApp._checkGasLimit, and it has to be greater than or equal tominDstGasLookup[_dstChainId][_type]. Even the non-blocking messaging channels can be blocked if theminDstGasLookupvalue is not sufficient enough. This can happen if the transaction reverts with Out Of Gas error before reaching the non-blocking app try/catch (e.g. somewhere here). As a result, failed transaction will be stored inEndpoint.storedPayloadinstead ofLzNonblockingApp.failedMessages.Recommendation
Ensure that the
minDstGasLookupvalue is set sufficiently high to prevent failed messages from blocking the channel. -
L-33 Low Hardcoded
LOCAL_CHAIN_IDConfiguration AcknowledgedDescription
The
LOCAL_CHAIN_IDfor the current chain inMultiRewardsClaimingHandleris hardcoded to0. If the user specifies 0 asdstChainId, the reward tokens will be simply transferred to them on the current chain. Typically, there is no reason for a user to go through that flow if they want to claim the reward tokens on the same chain, since they can do it by callingMultiRewards.getRewards()without passing any arguments. If for some reason an external integrator always callgetRewardswith parameters, it makes sense that when they want to claim on the same chain, they will pass its layer zero chain id asdstChainIdand not0. This will not be caught by the current if statement and the transaction will probably revert becauseminDstGasLookupfor the current chain will not be set in theOFT. Because of this, the user will not be able to claim their tokens.Recommendation
Consider having the LayerZero id on each chain as
LOCAL_CHAIN_IDinstead of hardcoding it to 0. You can additionally check for 0 as well if it's needed for some other integrations.
Remediation Review
9 findings · December 11 to 14, 2024-
H-01 High _getRewardsFor DOS DOS Resolved
Description
L-26 shows how in some cases a transfer of 0 amount can happen. It recommends to skip the current loop iteration if the current reward amount is 0 in
bothMultiRewards._getRewardsFor() and MultiRewardsClaimingHandler.notifyRewards(). The current implemented fix skips the iteration only inMultiRewards._getRewardsFor(). This fix introduces a more serious problem. InMultiRewardsif the current reward is 0, the entry in the_rewardsarray won't be populated, i.e itstokenandamountwill both be 0. The whole array will be passed torewardHandler.notifyRewards(). Inside that function, there are two possible scenarios: A) transfer to the same chain B) transfer to a different chain Both of these cases will lead to revert if the current token isaddress(0). For A,safeTransfer()will be called onaddress(0). And for B, the oft will be retrieved astokenOfts[address(0)]which will result inaddress(0)as well and will revert whensendFrom()is called. In result, core contract functionalities will be blocked for the given user until they accumulate rewards in each token listed.Recommendation
Skip the iteration in
MultiRewardsClaimingHandler.notifyRewards()if the current amount is 0. -
M-01 Medium Get Rewards DoS DoS Resolved
Description
In the
_getRewardsForfunction the_rewardsarray is now unpopulated when the reward amount is zero. As a result therewardTokenstored in the rewards entry will beaddress(0). This will lead to a DoS in thenotifyRewardsfunction in all cases. Firstly in theparam.dstChainId == LOCAL_CHAIN_IDcase will attempt to call the transfer function on the zero address which will revert. And in theparam.dstChainId != LOCAL_CHAIN_IDcase thetransferFromfunction is attempted to be called on theoftthat corresponds to thetoken. The correspondingoftwill also be the zero address and thus this call will subsequently revert.Recommendation
Add early continue cases for both the
token == address(0)andamount == 0for robust compatibility with all operators and tokens which might revert on zero transfer. -
M-02 Medium DoS Invalid Address(0) Reward Entry in
_getRewardsForDoS ResolvedDescription
In the
_getRewardsForfunction, if a particular reward amount is zero, the corresponding_rewards[i]entry remains unchanged and effectively defaults toaddress(0)andamount = 0. When thenotifyRewardsfunction subsequently processes the returned_rewardsarray, it attempts to perform a token transfer for that entry, causing the call to revert due to the invalid token address. This will disrupt normal reward distribution and prevent users from successfully receiving other valid rewards in the same batch.Recommendation
Update the
notifyRewardsfunction to check foraddress(0)before attempting any token transfers. If the token address isaddress(0), simply continue to the next iteration. -
L-01 Low Callback Token Reentrancy Reentrancy Resolved
Description
In the
stakeandstakeForfunctions thelastAddedmapping is updated after thesuper.stakeandsuper.stakeForcalls. As a result, if the staking token allows any callback hooks upontransferFrom, then it may be possible for the user to re-enter into theSpellPowerStakingsystem and withdraw their stake immediately without waiting the lockup period. There is no identified incentive to do this, and the planned staking tokens do not have callbacks, however as a best practice thelastAddedshould be updated before the call tosuper.stakeandsuper.stakeFor.Recommendation
Consider updating the
lastAddedmapping before the calls tosuper.stakeandsuper.stakeFor. -
L-02 Low Outdated Comment Documentation Resolved
Description
In the documentation for the
BoundSpellActionReceiveronOFTReceivedfunction the comment for the_payloadparameter still references therewardHandlerParams, however this value has been removed from theMintBoundSpellAndStakeParamsstruct.Recommendation
Consider removing the comment that references the
rewardHandlerParams. -
L-03 Low Unnecessary Import Optimization Resolved
Description
The
BoundSpellCrossChainActionsfile imports theRewardHandlerParamshowever these are not used in either contract in this file.Recommendation
Remove the
RewardHandlerParamsimport. -
L-04 Low
exitFunction Only Transfers Rewards TotoWarning ResolvedDescription
For the fix of L-25, the
_getRewardsForfunction now accepts atoaddress, allowing the reward recipient on the destination chain to be different from the user address on the source chain. The_exitFor andexitfunctions also accept atoaddress as a parameter to achieve this functionality.function _exitFor(address user, address to, RewardHandlerParams memory params) internal { _withdrawFor(user, balanceOf[user]); _getRewardsFor(user, to, params); }During the call, the
stakingTokenis withdrawn to theuseraddress, and rewards are transferred totoaddress. This behavior is intended as part of the fix for the previous L-25 issue. However, the same behavior is observed when the user calls the regularexit(address to, RewardHandlerParams memory params)function. Thisexitfunction gives the impression that all balances, including thestakingTokenbalance, will be transferred to the to address. However, only the rewards are transferred to thetoaddress, while thestakingTokenis still transferred to theuseraddress.Recommendation
If this behavior is intended, consider documenting it to avoid misunderstandings. Otherwise, transfer the
stakingTokento thetoaddress. -
L-05 Low Unused Error Informational Resolved
Description
ErrZeroAddresserror inMultiRewardsClaimingHandlercontract is defined but never used.Recommendation
Consider removing unused error.
-
L-06 Low Unused Import And Comment Informational Resolved
Description
rewardHandlerParamswas removed from theMintBoundSpellAndStakeParamsand is no longer used anywhere in the contract. However, therewardHandlerParamsimport statement was not removed, and comments in line 237 still reference it.Recommendation
Remove the
rewardHandlerParamsimport statement and delete the comment that mentionsrewardHandlerParams.
No findings match.
More from Abracadabra Money
All 19 reports-
MagicGLP Upgrade
11 findings1 high 11 findings: 1 high, 10 low -
PurrSwap Aerodrome
105 findings9 high 105 findings: 9 high, 23 medium, 45 low, 28 informational -
PurrSwap ve33
74 findings5 critical · 10 high 74 findings: 5 critical, 10 high, 20 medium, 39 low -
Hyperliquid OFT Deployment
3 findings 3 findings: 1 low, 2 informational
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.
