Organization
- @monad
Engagement Type
Cantina Reviews
Period
-
Repositories
Findings
Medium Risk
1 findings
1 fixed
0 acknowledged
Low Risk
9 findings
9 fixed
0 acknowledged
Informational
10 findings
8 fixed
2 acknowledged
Gas Optimizations
2 findings
2 fixed
0 acknowledged
Medium Risk1 finding
Griefing users can cause DoS by reverting native token transfers
State
Severity
- Severity: Medium
Submitted by
hash
Description
The number of users that can enter the raffle is intentionally limited to
MAX_PLATFORM_USERSto make the processing feasible. DoS on the supported user slots is prevented using a combination ofminimumBalancerequirement and the ability for the admin to forcefully remove the users using thereturnDeposits(...)function. But given thatreturnDeposits(...)uses_performTransfer(...)to send the user's balance, a user can revert that call and disallow the admin from forcefully removing them. This allows griefing users to squat in the supported user slots (without participating in any/many raffles) and prevent other legitimate users from participating in the raffles.function _performTransfer(address recipient_, uint256 value_) internal { (bool succeeded, ) = recipient_.call{ value: value_ }(""); if (!succeeded) { revert("Transfer failed"); } }Recommendation
Consider wrapping the native token and sending wrapped tokens in case of failed native token transfer. Also, limit the amount of gas that is passed during such a transfer.
Low Risk9 findings
Allowing updateRaffle(...) beyond PreDraw stage may cause unexpected behavior
State
Severity
- Severity: Low
Submitted by
0xRajeev
Description
PLATFORM_ADMIN_ROLEis allowed to callupdateRaffle(...)in any stage of the raffle to update aspects related toticketPrice,closedTimestamp,nftAddress,tokenIds,projectTreasuryandraffleFeePercentage. This supposedly is intended to mainly allow the project to correct any wrong list oftokenIdsprovided earlier, which may happen because NFT ownership is not validated earlier.However, this privileged action by
PLATFORM_ADMIN_ROLEmay cause unexpected behavior for users who may not observe such updates in time and therefore end up participating in raffles with updated parameters they did not accept earlier. This also allows a big window of opportunity for abuse by the platform, especially in stages beyondPreDraw.Possible scenario:
- Alice notices an upcoming raffle for a NFT collection she likes.
- Alice deposits sufficient funds to participate in this raffle.
- Platform later updates the NFT collection to a different one after raffle starts.
- Alice fails to notice the update and so is considered as a participant in the updated raffle for NFTs she does not like.
- Alice is one of the winners and pays the ticket price for an NFT token she did not mean to receive during sign up.
Recommendation
Consider restricting
updateRaffle(...)to onlyPreDrawstage so that all raffle parameters are locked beyond it, which retains a window to fix any errors but reduces the window for accidents or abuse.Intended maximum platform user limit is not allowed
State
Severity
- Severity: Low
Submitted by
0xRajeev
Description
The platform implements
constant MAX_PLATFORM_USERS = 10_000_000, which is meant to specify the maximum number of simultaneous users who can make use of this platform. And this limit allows the use of a fixed-length array forticketRanges, thereby reducing storage costs.However, the implemented check in
depositForUser(...)is:if (newKey && userBalances.length() > (MAX_PLATFORM_USERS - 1)) { revert("Max platform users reached");}where the
-1in bounds check reverts when we are still one less thanMAX_PLATFORM_USERSinstead of when we have reached it. Therefore, the intended maximum platform user limit is never reached.Recommendation
Consider fixing the check, for example, by removing
-1.Ignoring zero as a valid random number may prevent a raffle from completing
State
Severity
- Severity: Low
Submitted by
0xRajeev
Description
Minthub platform uses Pyth Network as the entropy source for random number seed generation used in raffles. It treats zero as the default value of
raffle.entropy.randomNumberassuming that zero is an invalid entropy result from Pyth Network. It enforces this assumption inentropyCallback(...)by checkingraffle.entropy.randomNumber == bytes32(0)to detect any repeated callbacks. It again enforces this indrawWinningTicketsBatch(...)by checking against 0 to ensure that it has indeed received randomness.However, while the probability of Pyth Network returning 0 as the random number is extremely small, it is still a valid result. Treating it as invalid will allow repeated callbacks and also prevent such a raffle from completing because of the enforced checks.
Recommendation
Reconsider treating zero as an invalid entropy returned from Pyth Network by making appropriate changes to related logic.
Prizes cannot be distributed when the raffle is cancelled after draw completion
State
Severity
- Severity: Low
Submitted by
hash
Description
Once the draw is complete i.e. the prizes are awarded, winners should be able to claim their reward even if the draw is cancelled for some reason by the
PLATFORM_ADMIN. But currently, this is not possible because_distributePrize(...)requires the raffle state to beDrawComplete.function _distributePrize( Raffle storage raffle_, uint256 raffleId_, address nftContract_, uint256 prizeIndex_ ) internal returns (bool transfered_) { PrizeItem storage prize = raffle_.prizes.items[prizeIndex_]; // We can't run this before we are at status DrawComplete. if (raffle_.status != RaffleStatus.DrawComplete) { revert("Draw is not complete"); }Also, the state of ticket-fee-paid but prize-not-distributed is possible in the current implementation. This is unfair for the users when raffles are cancelled in such a state because they won't be able to receive their prize NFTs for which they already had paid the fee.
Recommendation
Consider allowing distribution of prizes for raffles that are cancelled after draw completion, or alternatively preventing cancellations after draw completion.
Users may be griefed to force participate in raffles
State
Severity
- Severity: Low
Submitted by
0xRajeev
Description
Minthub supports a
depositForUser(...)function to allow deposits for other users for allowing functionality such as promotions where the user gets a balance in the raffle that is funded by promoters/donations. However, this functionality can be abused by griefers to force participate users in raffles leading to unexpected loss of their balances. For example, anyone can donate to a user who is belowminimumBalance(and expects to simply withdraw it) and hasParticipationListType == ALL, thereby forcing their participation in upcoming raffles where ticket prices >= balance.Example scenario:
- Alice has a balance of 49 MON on Minthub, where
minimumBalancehas been set to 50. She does not intent to participate in any future raffles but plans to withdraw her remaining balance. - Mallory notices an upcoming raffle where the ticket price is 50 MON and uses
depositForUser(...)to deposit 1 MON for Alice. - Alice's balance is now 50 MON and she is forced to participate in the upcoming raffle given that her balance buys her 1 ticket.
- If Alice is one of the raffle winners, she is deducted 50 MON and awarded the NFT.
- Alice is now left with an NFT from a raffle she did not intend to participate and loses her 49 MON balance she intended to withdraw.
Recommendation
Consider weighing the need for and access to this griefing vector against the benefits of the supported functionality.
Not refunding any excess Pyth fees may lead to loss of protocol funds
State
Severity
- Severity: Low
Submitted by
0xRajeev
Description
Minthub uses Pyth Network to seed entropy for the random number generation for raffles. To obtain this service, the protocol needs to pay fees to Pyth Network, which differs for every chain and also varies over time depending on the chain's current gas price.
Pyth's documented recommendation is to use its on-chain method
getFeeV2(...)to compute the required fee and send that amount as the value of therequestV2(...)call. However, Minthub's current design expects an offchain call togetFeeV2(...)and providing that asmsg.valuetostartDraw(...)for its call torequestV2(...). While this may work under stable chain/fee conditions, theBATCH_OPERATOR_ROLEmay have to overpay fees forstartDraw(...)calls during any periods of volatility. Any underpayment will cause a revert (which on Monad uses up the entire gas sent with the transaction and requires resending it with correct fees) and overpayments will not be refunded, which leads to loss of protocol operational funds used for gas and fees.Recommendation
Consider implementing Pyth's recommendation to use its on-chain method
getFeeV2(...)to compute the required fee and refunding any excessmsg.valuesent byBATCH_OPERATOR_ROLE.Missing ownership/existence checks for raffle NFTs may lead to unexpected behavior
State
Severity
- Severity: Low
Submitted by
0xRajeev
Description
Creating/updating of raffles checks if the NFT Treasury has approved Minthub to transfer NFTs. However, there are no ownership/existence checks for raffle NFTs. This supposedly is intentional to support flows where raffles are launched before the NFTs are in the NFT Treasury or before NFTs even exist.
While this is reasonable during raffle creation, the missing ownership/existence checks for raffle NFTs even in the later raffle stages may lead to NFT distribution failures if NFTs still do not exist or the NFT Treasury does not have their ownership. This would cause unexpected behavior for winning users who will not receive the previously advertised NFTs.
Recommendation
Consider adding ownership/existence checks for raffle NFTs before the
DrawStartedstage to ensure that NFT Treasury owns all the listed NFTs.Incorrect prize index emitted may affect offchain processing
State
Severity
- Severity: Low
Submitted by
0xRajeev
Description
The
prizeIndex_parameter used in eventTicketPaymentProcessedemitted in_processPayment(...)is derived fromprize.prizeIndex. However,prize.prizeIndexstores an incorrect iteration index when prizes are added to the prizes array in_awardPrize(...)for batched calls todrawWinningTicketsBatch(...). This is because the starting index is ignored for successive batched calls.If these events are consumed by offchain agents then they will associate an incorrect NFT prize with the winning tickets and addresses.
Recommendation
Consider using
raffle.prizes.items.lengthinstead forprizeIndexwhen pushingPrizeItemintoraffle.prizes.itemsarray, which is also the index used to determine the NFTtokenIDto be distributed.Incorrect ordering of PrizeAwarded event arguments
State
Severity
- Severity: Low
Submitted by
hash
Description
ticketNumberandprizeIndexparameters ofPrizeAwardedevent have their positions incorrectly exchanged in the emit.event PrizeAwarded( uint32 indexed raffleId, address indexed winner, uint32 prizeIndex, uint32 ticketNumber ); ..... emit PrizeAwarded(uint32(raffleId_), winner_, ticketNumber_, prizeIndex_);Recommendation
Change the ordering or change the event signature.
Informational10 findings
Redundant initializations can be removed
State
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
Initializing variables to their default values is redundant and consumes valuable bytecode space and gas.
Recommendation
Consider removing these initializations wherever possible.
Privileged functions are missing event emissions
State
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
Few of the privileged functions are missing event emissions. This prevents any offchain monitoring tools from observing these privileged actions and allowing users to react to them if necessary.
Recommendation
Consider emitting events for all privileged functions.
Draw completion process assumes stable balances of users but is not strictly enforced
State
Severity
- Severity: Informational
Submitted by
hash
Description
enumerateTicketsBatchanddrawWinningTicketsBatchrely onusersBalancesbeing stable and not getting altered in between for its correct working. Although this requires unexpected behavior from privileged entities, such scenarios might accidentally happen.Example scenarios include not obtaining the random number from Pyth, admin calling
returnDeposits, admin not completing the various steps of the draw within the time limits etc. (eg:distributeAllPrizesForRaffle (...)of a raffle being called when another raffle is already under draw).Recommendation
Ensure that the draw is cancelled if it cannot be fully completed before the allocated time limit.
Abraxas Minthub
Cantina
Across multiple commits, ending with Commit #4aba111, changes were made by adding logic for
depositBalancesLastChangedTimestamp,raffle.stableBalancesRequiredFromTimestamp,onlyWhenStableBalancesandonlyBatchOperatorWhenLockedto make the raffle process more robust against unexpected changes to user balances.distributeUserPrizes(...) performs unbounded array iteration
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
hash
Description
distributeUserPrizes(...)iterates through the specific user's entirewinningRafflesForUserand respectiveuserPrizesarrays. Since elements are not removed from the array even after claiming, these arrays could grow to large lengths even under normal operations and could theoretically grow to enough elements to exhaust the block gas limit to perform the full iteration.Recommendation
Consider adding a batch variant which limits the number of iterations per call.
Abraxas
Acknowledged.
Claims are always possible through more gas efficient methods, for example
distributeSinglePrize. The methoddistributeUserPrizes(…)is provided solely as a convenience function for users with prizes across multiple raffles. Under expected processing all prizes will be distributed usingdistributeAllPrizesForRaffleBatchwith no need to calldistributeUserPrizes(…).Cantina
Acknowledged.
Fee is captured even when admin is returning funds to users
State
Severity
- Severity: Informational
Submitted by
hash
Description
Even when users are not withdrawing themselves and deposits are being returned by the admin using the
returnDeposits(...)function, the platform fee is still charged from users. And the admin can return deposits of any user irrespective of whether the user has> minimumBalanceor not.Recommendation
Consider not capturing the fee or disallow
returnDeposits(...)for balances >minimumBalance.Setters are missing idempotent checks
State
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
A defensive coding style is to add idempotent checks for when the new value is same as current value, especially for booleans. This allows timely detection of any inconsistency between onchain values and their offchain tracking.
Setters
setDrawsPausedStatus(...)andsetDepositsPausedStatus(...)are missing such idempotent checks.Recommendation
Consider adding idempotent checks to setters.
Deposit event parameters can be indexed
State
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
Deposit event parameters of
calleranduseraddresses are missing theindexedattribute to allow faster lookups.Recommendation
Consider adding the
indexedattribute to Deposit event parameters.LogRaffleStatus incorrectly logs the raffle status always as RaffleStatus.PreDraw
State
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
While
updateRaffle(...)can be called in any raffle stage,LogRaffleStatusincorrectly logs the raffle status always asRaffleStatus.PreDraw. This will be incorrect ifupdateRaffle(...)is called in later stages.Recommendation
Consider logging
raffle.statusinstead ofRaffleStatus.PreDrawto reflect the current raffle stage.Comment mentions _lockOff as function name instead of _unlock
State
Severity
- Severity: Informational
Submitted by
hash
Description
The Natspec mentions the function name incorrectly as
_lockOffinstead of_unlock.* @dev _lockOff Manually unlocks by setting the unlockAtTimestamp to block.timestamp. */ function _unlock() internal { unlockAtTimestamp = block.timestamp; }Recommendation
Change
_lockOffto_unlockin the comment.Compromised/Malfunctional privileged roles can cause unexpected behavior
State
- Acknowledged
Severity
- Severity: Informational
Submitted by
0xRajeev
Description
Minthub has privileged roles of
DEFAULT_ADMIN_ROLE,PLATFORM_ADMIN_ROLE,RAFFLE_ADMIN_ROLEandBATCH_OPERATOR_ROLE.DEFAULT_ADMIN_ROLEcan grant and revoke membership of other roles.PLATFORM_ADMIN_ROLEcan set the minimum balance, NFT treasury address, platform fee treasury address, platform fee percentage, pause/unpause draws and deposits, cancel a raffle, unlock, update the details of a raffle, rescue ERC721 and ERC20 tokens and return all deposits to users.RAFFLE_ADMIN_ROLEcan create raffles.BATCH_OPERATOR_ROLEcan call batch operations that conduct the raffle draw, enumeration and distribution process.Any compromised/malfunctional privileged role can cause unexpected behavior if it calls its authorized functions with incorrect values or in a delayed manner accidentally/intentionally. While there are implemented safeguards to mitigate many of these situations, this nevertheless poses a centralization risk.
Recommendation
Consider:
- Implementing the highest levels of operational security for privileged role management.
- Documenting and highlighting the assumptions and risks for platform users/owners.
Abraxas
Acknowledged.
We fully concur with these recommendations and have impressed upon the platform owner the need to implement stringent security for the privilege roles, in particular
DEFAULT_ADMIN_ROLEandPLATFORM_ADMIN_ROLE. This recommendation was highlighted in our code handover and is prominently made in the Authority Model section of the repository readme.Cantina
Acknowledged.
Gas Optimizations2 findings
Loop iteration conditional bound can be cached
State
Severity
- Severity: Gas optimization
Submitted by
0xRajeev
Description
There are many loops where the iteration conditional bound is always evaluated to determine the length. This can be cached to save gas.
Recommendation
Consider caching the loop iteration conditional bound length.
Incorrect ordering of arguments to _gcd() causes 1 extra iteration
State
Severity
- Severity: Gas optimization
Submitted by
hash
Description
The
_gcd()function expects the bigger number to be passed in as the first argument. But currently, the smaller numbercandidate_is passed as the first argument instead of N. This results in an unwanted additional iteration.if (_gcd(candidate_, N) == 1) {Recommendation
Pass
Nas the first argument.