Skip to content

Instantly share code, notes, and snippets.

@ChaseTheLight01
Created May 28, 2024 13:42
Show Gist options
  • Save ChaseTheLight01/9831938523d6387a6cb53a1cef52fe36 to your computer and use it in GitHub Desktop.
Save ChaseTheLight01/9831938523d6387a6cb53a1cef52fe36 to your computer and use it in GitHub Desktop.
LightChaserV3_Cantina_YoloGames

LightChaser-V3

Generated for: Cantina : YoloGames

Generated on: 2024-05-28

Total findings: 100

Total Medium findings: 4

Total Low findings: 20

Total Gas findings: 43

Total NonCritical findings: 33

Summary for Medium findings

Number Details Instances
[Medium-1] Polygon chain reorgs affect Chainlink randomness 4
[Medium-2] [V4] deposit/redeem functions found which implement accounting arithmetic vulnerable to the donation attack 7
[Medium-3] [V4] Withdrawal mechanism can be Dos'd with dust withdrawals 2
[Medium-4] Privileged functions can create points of failure 19

Summary for Low findings

Number Details Instances
[Low-1] Potential division by zero should have zero checks in place 4
[Low-2] Subtraction may underflow if multiplication is too large 2
[Low-3] Missing checks for address(0x0) when updating address state variables 5
[Low-4] Code does not follow the best practice of check-effects-interaction 9
[Low-5] Do not use block values to generate randomness 5
[Low-6] Contract contains payable functions but no withdraw/sweep function 6
[Low-7] Using block.number is not fully L2 compatible 8
[Low-8] Empty receive functions can cause gas issues 1
[Low-9] Loss of precision 5
[Low-10] Missing zero address check in constructor 8
[Low-11] Using zero as a parameter 6
[Low-12] Contract can be bricked by the use of both 'Ownable' and 'Pausable' in the same contract 1
[Low-13] Constant decimal values 9
[Low-14] No access control on receive/payable fallback 1
[Low-15] Critical functions should have a timelock 9
[Low-16] Consider implementing two-step procedure for updating protocol addresses 2
[Low-17] Don't assume specific ETH balance 5
[Low-18] State variables not capped at reasonable values 1
[Low-19] Consider a uptime feed on L2 deployments to prevent issues caused by downtime 10
[Low-20] Non constant/immutable state variables are missing a setter post deployment 2

Summary for NonCritical findings

Number Details Instances
[NonCritical-1] Important function with no access control 1
[NonCritical-2] Unnecessary struct attribute prefix 2
[NonCritical-3] Using abi.encodePacked can result in hash collision when used in hashing functions 2
[NonCritical-4] Default address(0) can be returned 1
[NonCritical-5] Owner can renounce while system is paused 2
[NonCritical-6] Events regarding state variable changes should emit the previous state variable value 10
[NonCritical-7] In functions which accept an address as a parameter, there should be a zero address check to prevent bugs 47
[NonCritical-8] Enum values should be used in place of constant array indexes 4
[NonCritical-9] Revert statements within external and public functions can be used to perform DOS attacks 27
[NonCritical-10] Contract lines should not be longer than 120 characters for readability 10
[NonCritical-11] Avoid updating storage when the value hasn't changed 9
[NonCritical-12] Not all event definitions are utilizing indexed variables. 32
[NonCritical-13] Contracts should have all public/external functions exposed by interfaces 56
[NonCritical-14] Functions within contracts are not ordered according to the solidity style guide 3
[NonCritical-15] Functions with array parameters should have length checks in place 4
[NonCritical-16] Constants should be on the left side of the comparison 40
[NonCritical-17] Overly complicated arithmetic 3
[NonCritical-18] Use of non-named numeric constants 44
[NonCritical-19] Employ Explicit Casting to Bytes or Bytes32 for Enhanced Code Clarity and Meaning 2
[NonCritical-20] Cyclomatic complexity in functions 15
[NonCritical-21] Events may be emitted out of order due to code not follow the best practice of check-effects-interaction 11
[NonCritical-22] Missing events in sensitive functions 2
[NonCritical-23] Unchecked increments can overflow 1
[NonCritical-24] Avoid mutating function parameters 7
[NonCritical-25] Avoid hard coding gasLimit values 2
[NonCritical-26] Contracts use both += 1 and ++ (-- and -= 1) 1
[NonCritical-27] Long numbers should include underscores to improve readability and prevent typos 2
[NonCritical-28] Avoid declaring variables with the names of defined functions within the project 1
[NonCritical-29] Constructors should emit an event 7
[NonCritical-30] Constructor with array/string/bytes parameters has no empty array checks 3
[NonCritical-31] Errors should have parameters 94
[NonCritical-32] Avoid using 'owner' or '_owner' as a parameter name 2
[NonCritical-33] Avoid arithmetic directly within array indices 1

Summary for Gas findings

Number Details Instances Gas
[Gas-1] Multiple accesses of the same mapping/array key/index should be cached 2 252
[Gas-2] bytes.concat() can be used in place of abi.encodePacked 2 0.0
[Gas-3] Using named returns for pure and view functions is cheaper than using regular returns 4 416
[Gas-4] Functions which only revert serve no purpose so consider removing them to save GAS 2 0.0
[Gas-5] Public functions not used internally can be marked as external to save gas 1 0.0
[Gas-6] Usage of smaller uint/int types causes overhead 43 101695
[Gas-7] Mappings are cheaper than arrays for state variable iteration 3 9000
[Gas-8] Use != 0 instead of > 0 7 147
[Gas-9] Integer increments by one can be unchecked to save on gas fees 3 1080
[Gas-10] Default int values are manually reset 2 0.0
[Gas-11] Function calls within for loops 3 0.0
[Gas-12] For loops in public or external functions should be avoided due to high gas costs and possible DOS 1 0.0
[Gas-13] Mappings used within a function more than once should be cached to save gas 4 1600
[Gas-14] Use assembly to check for the zero address 18 0.0
[Gas-15] Divisions which do not divide by -X cannot overflow or underflow so such operations can be unchecked to save gas 2 0.0
[Gas-16] Divisions of powers of 2 can be replaced by a right shift operation to save gas 2 0.0
[Gas-17] multiplications of powers of 2 can be replaced by a left shift operation to save gas 1 0.0
[Gas-18] Structs can be packed into fewer storage slots 6 90000
[Gas-19] Superfluous event fields 14 7140
[Gas-20] Private functions used once can be inlined 3 0.0
[Gas-21] Consider using OZ EnumerateSet in place of nested mappings 2 4000
[Gas-22] Use assembly to emit events 40 60800
[Gas-23] Use solady library where possible to save gas 4 16000
[Gas-24] Using private rather than public for constants and immutables, saves gas 3 0.0
[Gas-25] Modulus operations that could be unchecked 2 340
[Gas-26] Mark Functions That Revert For Normal Users As payable 19 9025
[Gas-27] Lack of unchecked in loops 8 9600
[Gas-28] Using nested if to save gas 3 54
[Gas-29] Optimize Storage with Byte Truncation for Time Related State Variables 1 2000
[Gas-30] Using delete instead of setting mapping to 0 saves gas 1 5
[Gas-31] Using delete instead of setting struct to 0 saves gas 1 5
[Gas-32] Stack variable cost less than state variables while used in emiting event 2 36
[Gas-33] Variable declared within iteration 4 0.0
[Gas-34] Internal functions only used once can be inlined to save gas 1 30
[Gas-35] Constructors can be marked as payable to save deployment gas 9 0.0
[Gas-36] Assigning to structs can be more efficient 12 18720
[Gas-37] An inefficient way of checking if a integer is even is being used (X % 2 == 0) 3 0.0
[Gas-38] Only emit event in setter function if the state variable was changed 8 0.0
[Gas-39] It is a waste of GAS to emit variable literals 2 32
[Gas-40] Use OZ Array.unsafeAccess() to avoid repeated array length checks 12 302400
[Gas-41] State variable read in a loop 1 63734
[Gas-42] Consider pre-calculating the address of address(this) to save gas 17 0.0
[Gas-43] Public functions not called internally 1 0.0

[Medium-1] Polygon chain reorgs affect Chainlink randomness

Resolution

The requestConfirmations value in VRFv2Consumer is currently set to a low number. This value is critical as it communicates to the Chainlink VRF service the minimum number of blocks to wait before providing randomness. Its purpose is to mitigate the impact of chain reorganizations, events where block and transaction orders are rearranged, thus changing their output. This issue is particularly relevant for applications slated for deployment on Polygon, as referenced in the README.md, given the frequency and depth of reorganizations on this network, often exceeding 3 blocks. Even more alarmingly, a recent event revealed a chain reorganization on Polygon with a depth of 156 blocks. This scenario could lead to the possibility of changing the winner of a lootbox game. If a transaction requesting randomness from VRF is displaced to a different block due to a reorganization, the resultant randomness would also change. Hence, it may be prudent to revisit and adjust the REQUEST_CONFIRMATION constant to a higher value to ensure consistent and accurate outcomes.

Num of instances: 4

Findings

Click to show findings

['367']

367:         vrfParameters.minimumRequestConfirmations = _vrfParameters.minimumRequestConfirmations; // <= FOUND

['204']

204:         uint256 vrfFee = _requestRandomness(); // <= FOUND

['281']

281:         _requestRandomness(); // <= FOUND

['229']

229:         uint256 requestId = VRFCoordinatorV2Interface(coordinator).requestRandomWords({ // <= FOUND
230:             keyHash: keyHash,
231:             subId: subscriptionId,
232:             minimumRequestConfirmations: minimumRequestConfirmations,
233:             callbackGasLimit: callbackGasLimit,
234:             numWords: uint32(1)
235:         });

[Medium-2] [V4] deposit/redeem functions found which implement accounting arithmetic vulnerable to the donation attack

Resolution

Calculations using non-internal accounting, such as balanceOf or totalSupply, can introduce potential donation attack vectors. For instance, consider a scenario where a reward calculation uses totalSupply during a deposit. An attacker could exploit this by donating a large amount of tokens to the vault before the user tries to redeem their withdrawal. This manipulation causes the totalSupply to change significantly, altering the reward calculations and potentially leading to unexpected or unfair outcomes for users.

Let's say a smart contract calculates user rewards based on their share of the total token supply:

uint256 reward = (userDeposit * totalRewards) / totalSupply;

If an attacker donates a large number of tokens to the vault before users withdraw, totalSupply increases, thereby diluting each user's share of the rewards. This discrepancy between expected and actual rewards can undermine user trust and contract integrity.

The risks associated with this attack include reward manipulation, where attackers can alter totalSupply to reduce the share of honest users, and economic attacks, where significant token donations destabilize the contract's economic assumptions, leading to unfair advantages or financial losses. Additionally, users may find their rewards significantly lower than expected, causing confusion and dissatisfaction.

To prevent such vulnerabilities, it's essential to use internal variables to track deposits, withdrawals, and rewards instead of relying on balanceOf or totalSupply. This approach isolates contract logic from external token movements. Implementing a snapshot mechanism to capture totalSupply at specific points in time ensures consistent reward calculations. Introducing limits on the number of tokens that can be deposited or donated in a single transaction can also prevent significant manipulation.

Num of instances: 7

Findings

Click to show findings

['61']

61:     function transferPayoutToPlayer( // <= FOUND
62:         address game,
63:         uint256 amount,
64:         address receiver
65:     ) external nonReentrant whenNotPaused {
66:         _onlyGameConfigurationManager();
67:         address currency = asset();
68:         uint256 balance = IERC20(currency).balanceOf(address(this));
69:         if (balance < amount) {
70:             emit InsufficientFundsForPayout(game, receiver, currency, amount - balance);
71:             amount = balance;
72:         }
73:         _executeERC20DirectTransfer(currency, receiver, amount);
74:         emit PayoutTransferred(game, receiver, currency, amount);
75:     }

['55']

55:     function transferPayoutToPlayer( // <= FOUND
56:         address game,
57:         uint256 amount,
58:         address receiver
59:     ) external nonReentrant whenNotPaused {
60:         _onlyGameConfigurationManager();
61: 
62:         address weth = asset();
63: 
64:         uint256 balance = IERC20(weth).balanceOf(address(this));
65:         if (balance < amount) {
66:             emit InsufficientFundsForPayout(game, receiver, address(0), amount - balance);
67:             amount = balance;
68:         }
69: 
70:         IWETH(weth).withdraw(amount);
71: 
72:         _transferETHAndWrapIfFailWithGasLimit(weth, receiver, amount, 2_300);
73: 
74:         emit PayoutTransferred(game, receiver, address(0), amount);
75:     }

['249']

249:     function _handlePayout(
250:         address player,
251:         Game__GameParams storage params,
252:         uint256 numberOfRoundsPlayed,
253:         uint256 payout,
254:         uint256 protocolFee
255:     ) internal {
256:         payout += params.playAmountPerRound * (params.numberOfRounds - numberOfRoundsPlayed);
257:         _transferPlayAmountToPool(params.currency, params.playAmountPerRound * params.numberOfRounds);
258:         if (payout > 0) {
259:             GAME_CONFIGURATION_MANAGER.transferPayoutToPlayer(params.currency, payout, player); // <= FOUND
260:         }
261:         if (protocolFee > 0) {
262:             GAME_CONFIGURATION_MANAGER.transferProtocolFee(params.currency, protocolFee); // <= FOUND
263:         }
264:     }

['313']

313:     function transferPayoutToPlayer(address currency, uint256 amount, address receiver) external { // <= FOUND
314:         address liquidityPool = gameLiquidityPool[msg.sender][currency];
315:         if (liquidityPool == address(0)) {
316:             revert GameConfigurationManager__GameIsNotAllowed(msg.sender, currency);
317:         }
318: 
319:         ILiquidityPool(liquidityPool).transferPayoutToPlayer(msg.sender, amount, receiver); // <= FOUND
320:     }

['80']

80:     function transferProtocolFee( // <= FOUND
81:         address game,
82:         uint256 amount,
83:         address protocolFeeRecipient
84:     ) external nonReentrant whenNotPaused {
85:         _onlyGameConfigurationManager();
86:         address currency = asset();
87:         uint256 balance = IERC20(currency).balanceOf(address(this));
88:         if (balance >= amount) {
89:             _executeERC20DirectTransfer(currency, protocolFeeRecipient, amount);
90:             emit ProtocolFeeTransferred(game, protocolFeeRecipient, currency, amount);
91:         }
92:     }

['80']

80:     function transferProtocolFee( // <= FOUND
81:         address game,
82:         uint256 amount,
83:         address protocolFeeRecipient
84:     ) external nonReentrant whenNotPaused {
85:         _onlyGameConfigurationManager();
86:         address weth = asset();
87:         uint256 balance = IERC20(weth).balanceOf(address(this));
88:         if (balance >= amount) {
89:             IWETH(weth).withdraw(amount);
90:             _transferETHAndWrapIfFailWithGasLimit(weth, protocolFeeRecipient, amount, gasleft());
91:             emit ProtocolFeeTransferred(game, protocolFeeRecipient, address(0), amount);
92:         }
93:     }

['181']

181:     function _liquidityPoolBalance(address currency) internal view returns (uint256 balance) { // <= FOUND
182:         address liquidityPool = _getGameLiquidityPool(currency);
183:         if (currency == address(0)) {
184:             currency = WETH;
185:         }
186:         balance = IERC20(currency).balanceOf(liquidityPool);
187:     }

[Medium-3] [V4] Withdrawal mechanism can be Dos'd with dust withdrawals

Resolution

As the 'amount' value is not validated against a minimum amount, a potential attacker can make a large number of micro withdrawal requests such a 1e1 for a token with 1e18 decimals. As such they can prevent other users from withdrawing their assets. A resolution to this is implementing a minimum withdrawal amount, although this will need setter limits of it's own to prevent it from being changed to a number so high, no one can withdraw.

Num of instances: 2

Findings

Click to show findings

['420']

420:     function redeem(address token, uint256 amount) external payable nonReentrant {
421:         address liquidityPool = _getLiquidityPoolOrRevert(token);
422: 
423:         _validateFinalizationIncentivePayment();
424: 
425:         if (redemptions[msg.sender].shares != 0) {
426:             revert LiquidityPoolRouter__OngoingRedemption();
427:         }
428: 
429:         TRANSFER_MANAGER.transferERC20(liquidityPool, msg.sender, address(this), amount); // <= FOUND
430: 
431:         uint256 expectedAssets = ERC4626(liquidityPool).previewRedeem(amount);
432: 
433:         redemptions[msg.sender] = Redemption(
434:             liquidityPool,
435:             amount,
436:             expectedAssets,
437:             block.timestamp,
438:             finalizationParams.finalizationIncentive
439:         );
440: 
441:         emit LiquidityPoolRouter__RedemptionInitialized(
442:             msg.sender,
443:             liquidityPool,
444:             amount,
445:             expectedAssets,
446:             finalizationParams.finalizationIncentive
447:         );
448:     }

['536']

536:     function _transferAssetsRedeemed(address token, address redeemer, uint256 assetsRedeemed) private { // <= FOUND
537:         if (token == WETH) {
538:             IWETH(WETH).withdraw(assetsRedeemed);
539:             _transferETHAndWrapIfFailWithGasLimit(WETH, redeemer, assetsRedeemed, 2_300); // <= FOUND
540:         } else {
541:             _executeERC20DirectTransfer(token, redeemer, assetsRedeemed); // <= FOUND
542:         }
543:     }

[Medium-4] Privileged functions can create points of failure

Resolution

Ensure such accounts are protected and consider implementing multi sig to prevent a single point of failure

Num of instances: 19

Findings

Click to show findings

['112']

112:     function claimYield(address receiver) external onlyOwner  // <= FOUND

['164']

164:     function initiateGameLiquidityPoolConnectionRequest(
165:         address game,
166:         address currency,
167:         address liquidityPool
168:     ) external onlyOwner  // <= FOUND

['189']

189:     function confirmGameLiquidityPoolConnectionRequest(
190:         address game,
191:         address currency,
192:         address liquidityPool
193:     ) external onlyOwner  // <= FOUND

['214']

214:     function disconnectGameFromLiquidityPool(address game, address currency) external onlyOwner  // <= FOUND

['225']

225:     function setElapsedTimeRequiredForRefund(uint40 _elapsedTimeRequiredForRefund) external onlyOwner  // <= FOUND

['242']

242:     function setMaximumNumberOfRounds(uint16 _maximumNumberOfRounds) external onlyOwner  // <= FOUND

['250']

250:     function setGameKellyFractionBasisPoints(address game, uint256 basisPoints) external onlyOwner  // <= FOUND

['262']

262:     function setVrfParameters(VrfParameters memory _vrfParameters) external onlyOwner  // <= FOUND

['270']

270:     function setVrfFeeRecipient(address _vrfFeeRecipient) external onlyOwner  // <= FOUND

['279']

279:     function setProtocolFeeRecipient(address _protocolFeeRecipient) external onlyOwner  // <= FOUND

['293']

293:     function setFeeSplit(
294:         address _game,
295:         uint16 _protocolFeeBasisPoints,
296:         uint16 _liquidityPoolFeeBasisPoints
297:     ) external onlyOwner  // <= FOUND

['117']

117:     function setMultipliers(uint128 riskLevel, uint128 rowCount, uint256[] memory _multipliers) external onlyOwner  // <= FOUND

['104']

104:     function togglePaused() external onlyOwner  // <= FOUND

['112']

112:     function claimYield(address receiver) external onlyOwner  // <= FOUND

['228']

228:     function addLiquidityPool(address liquidityPool) external onlyOwner  // <= FOUND

['245']

245:     function setDepositLimit(
246:         address liquidityPool,
247:         uint256 minDepositAmount,
248:         uint256 maxDepositAmount,
249:         uint256 maxBalance
250:     ) external onlyOwner  // <= FOUND

['270']

270:     function setFinalizationParams(
271:         uint80 _timelockDelay,
272:         uint80 _finalizationForAllDelay,
273:         uint80 _finalizationIncentive
274:     ) external onlyOwner  // <= FOUND

['112']

112:     function claimYield(address receiver) external onlyOwner  // <= FOUND

['124']

124:     function _onlyGameConfigurationManager() internal view  // <= FOUND

[Low-1] Potential division by zero should have zero checks in place

Resolution

Implement a zero address check for found instances

Num of instances: 4

Findings

Click to show findings

['294']

294:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
295:         address player = randomnessRequests[requestId];
296: 
297:         if (player != address(0)) { // <= FOUND
298:             LaserBlast__Game storage game = games[player];
299:             if (_hasLiquidityPool(game.params.currency)) { // <= FOUND
300:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) { // <= FOUND
301:                     randomnessRequests[requestId] = address(0);
302: 
303:                     RunningGameState memory runningGameState;
304:                     runningGameState.payouts = new uint256[](game.params.numberOfRounds);
305:                     runningGameState.randomWord = randomWords[0];
306:                     uint256[] memory results = new uint256[](game.params.numberOfRounds);
307: 
308:                     IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
309:                         address(this)
310:                     );
311: 
312:                     uint256 adjustedLiquidityPoolFeeBasisPoints = _applyMultiplierToLiquidityPoolFeeBasisPoints(
313:                         feeSplit.liquidityPoolFeeBasisPoints,
314:                         game.riskLevel
315:                     );
316: 
317:                     Fee memory fee;
318:                     uint256 playAmountPerRound = game.params.playAmountPerRound;
319: 
320:                     for (
321:                         ;
322:                         runningGameState.numberOfRoundsPlayed < game.params.numberOfRounds;
323:                         ++runningGameState.numberOfRoundsPlayed
324:                     ) {
325:                         if ( // <= FOUND
326:                             _stopGainOrStopLossHit(
327:                                 game.params.stopGain,
328:                                 game.params.stopLoss,
329:                                 runningGameState.netAmount
330:                             )
331:                         ) {
332:                             break;
333:                         }
334: 
335:                         (uint256 result, uint256 multiplier, uint256 randomWordForNextRound) = _dropTheBall(
336:                             game.riskLevel,
337:                             game.rowCount,
338:                             runningGameState.randomWord
339:                         );
340: 
341:                         uint256 protocolFee = (playAmountPerRound * multiplier * feeSplit.protocolFeeBasisPoints) / 1e8;
342:                         uint256 liquidityPoolFee = (playAmountPerRound *
343:                             multiplier *
344:                             adjustedLiquidityPoolFeeBasisPoints) / 1e8;
345: 
346:                         runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
347:                             (playAmountPerRound * multiplier) /
348:                             10_000 -
349:                             protocolFee -
350:                             liquidityPoolFee;
351:                         runningGameState.payout += runningGameState.payouts[runningGameState.numberOfRoundsPlayed];
352:                         runningGameState.netAmount += (int256(
353:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed]
354:                         ) - int256(playAmountPerRound));
355:                         fee.protocolFee += protocolFee;
356:                         fee.liquidityPoolFee += liquidityPoolFee;
357:                         results[runningGameState.numberOfRoundsPlayed] = result;
358: 
359:                         runningGameState.randomWord = randomWordForNextRound;
360:                     }
361: 
362:                     _handlePayout(
363:                         player,
364:                         game.params,
365:                         runningGameState.numberOfRoundsPlayed,
366:                         runningGameState.payout,
367:                         fee.protocolFee
368:                     );
369:                     _transferVrfFee(game.params.vrfFee);
370: 
371:                     emit LaserBlast__GameCompleted(
372:                         game.params.blockNumber,
373:                         player,
374:                         results,
375:                         runningGameState.payouts,
376:                         runningGameState.numberOfRoundsPlayed,
377:                         fee.protocolFee,
378:                         fee.liquidityPoolFee
379:                     );
380: 
381:                     _deleteGame(player);
382:                 }
383:             }
384:         }
385:     }

['585']

585:     function _getMultiplier(uint256 winProbability) private pure returns (uint256 multiplier) { // <= FOUND
586:         multiplier = (10_000 * 1e18) / winProbability; // <= FOUND
587:     }

['188']

188:     function kellyFraction() public view returns (uint256) { // <= FOUND
189:         uint256 multiplier = (_liquidityProviderAdjustedReturn() * 10_000) / 5_000 - 10_000;
190: 
191:         return (5_000 * KELLY_FRACTION_SCALER) / multiplier - (5_000 * KELLY_FRACTION_SCALER) / 10_000; // <= FOUND
192:     }

['333']

333:     function calculateWinProbability(uint256 multiplier) public pure returns (uint256 winProbability) { // <= FOUND
334:         winProbability = 100_000_000_000 / multiplier; // <= FOUND
335:     }

[Low-2] Subtraction may underflow if multiplication is too large

Resolution

In arithmetic operations involving subtraction and multiplication, an underflow may occur if a subtraction result is negative, or if a multiplication result exceeds the maximum value representable in the data type. For instance, if a large multiplication precedes a subtraction, it may create a value too large to subtract from, causing an underflow. This could lead to unexpected and incorrect results in the calculation.

Num of instances: 2

Findings

Click to show findings

['188']

188:     function kellyFraction() public view returns (uint256) { // <= FOUND
189:         uint256 multiplier = (_liquidityProviderAdjustedReturn() * 10_000) / 5_000 - 10_000;
190: 
191:         return (5_000 * KELLY_FRACTION_SCALER) / multiplier - (5_000 * KELLY_FRACTION_SCALER) / 10_000; // <= FOUND
192:     }

['223']

223:     function kellyFraction(uint256 multiplier) public view returns (uint256) { // <= FOUND
224:         _validateMultiplier(multiplier);
225: 
226:         uint256 winProbability = calculateWinProbability(multiplier);
227:         return // <= FOUND
228:             ((TOTAL_OUTCOMES - winProbability) * KELLY_FRACTION_SCALER) /
229:             ((multiplier * _liquidityProviderAdjustedReturn() * 1_000) / 10_000 - TOTAL_OUTCOMES) -
230:             (winProbability * KELLY_FRACTION_SCALER) /
231:             TOTAL_OUTCOMES;
232:     }

[Low-3] Missing checks for address(0x0) when updating address state variables

Num of instances: 5

Findings

Click to show findings

['250']

250:     function setGameKellyFractionBasisPoints(address game, uint256 basisPoints) external onlyOwner {
251:         if (basisPoints > 10_000) {
252:             revert GameConfigurationManager__BasisPointsTooHigh();
253:         }
254:         kellyFractionBasisPoints[game] = basisPoints;
255:         emit GameKellyFractionBasisPointsUpdated(game, basisPoints);
256:     }

['270']

270:     function setVrfFeeRecipient(address _vrfFeeRecipient) external onlyOwner {
271:         vrfFeeRecipient = _vrfFeeRecipient;
272:         emit VrfFeeRecipientUpdated(_vrfFeeRecipient);
273:     }

['279']

279:     function setProtocolFeeRecipient(address _protocolFeeRecipient) external onlyOwner {
280:         protocolFeeRecipient = _protocolFeeRecipient;
281:         emit ProtocolFeeRecipientUpdated(_protocolFeeRecipient);
282:     }

['293']

293:     function setFeeSplit(
294:         address _game,
295:         uint16 _protocolFeeBasisPoints,
296:         uint16 _liquidityPoolFeeBasisPoints
297:     ) external onlyOwner {
298:         if (_protocolFeeBasisPoints + _liquidityPoolFeeBasisPoints > 500) {
299:             revert GameConfigurationManager__BasisPointsTooHigh();
300:         }
301: 
302:         feeSplit[_game] = FeeSplit({
303:             protocolFeeBasisPoints: _protocolFeeBasisPoints,
304:             liquidityPoolFeeBasisPoints: _liquidityPoolFeeBasisPoints
305:         });
306: 
307:         emit FeeSplitUpdated(_game, _protocolFeeBasisPoints, _liquidityPoolFeeBasisPoints);
308:     }

['245']

245:     function setDepositLimit(
246:         address liquidityPool,
247:         uint256 minDepositAmount,
248:         uint256 maxDepositAmount,
249:         uint256 maxBalance
250:     ) external onlyOwner {
251:         if (minDepositAmount > maxDepositAmount) {
252:             revert LiquidityPoolRouter__MinDepositAmountTooHigh();
253:         }
254: 
255:         if (maxBalance < maxDepositAmount) {
256:             revert LiquidityPoolRouter__MaxDepositAmountTooHigh();
257:         }
258: 
259:         depositLimit[liquidityPool] = DepositLimit(minDepositAmount, maxDepositAmount, maxBalance);
260:         emit LiquidityPoolRouter__DepositLimitUpdated(liquidityPool, minDepositAmount, maxDepositAmount, maxBalance);
261:     }

[Low-4] Code does not follow the best practice of check-effects-interaction

Resolution

The "check-effects-interaction" pattern is a best practice in smart contract development, emphasizing the order of operations in functions to prevent reentrancy attacks. Violations arise when a function interacts with external contracts before settling internal state changes or checks. This misordering can expose the contract to potential threats. To adhere to this pattern, first ensure all conditions or checks are satisfied, then update any internal states, and only after these steps, interact with external contracts or addresses. Rearranging operations to this recommended sequence bolsters contract security and aligns with established best practices in the Ethereum community.

Num of instances: 9

Findings

Click to show findings

['61']

61:     function transferPayoutToPlayer(
62:         address game,
63:         uint256 amount,
64:         address receiver
65:     ) external nonReentrant whenNotPaused {
66:         _onlyGameConfigurationManager();
67:         address currency = asset();
68:         uint256 balance = IERC20(currency).balanceOf(address(this)); // <= FOUND
69:         if (balance < amount) {
70:             emit InsufficientFundsForPayout(game, receiver, currency, amount - balance);
71:             amount = balance;
72:         }
73:         _executeERC20DirectTransfer(currency, receiver, amount);
74:         emit PayoutTransferred(game, receiver, currency, amount);
75:     }

['55']

55:     function transferPayoutToPlayer(
56:         address game,
57:         uint256 amount,
58:         address receiver
59:     ) external nonReentrant whenNotPaused {
60:         _onlyGameConfigurationManager();
61: 
62:         address weth = asset();
63: 
64:         uint256 balance = IERC20(weth).balanceOf(address(this)); // <= FOUND
65:         if (balance < amount) {
66:             emit InsufficientFundsForPayout(game, receiver, address(0), amount - balance);
67:             amount = balance;
68:         }
69: 
70:         IWETH(weth).withdraw(amount); // <= FOUND
71: 
72:         _transferETHAndWrapIfFailWithGasLimit(weth, receiver, amount, 2_300);
73: 
74:         emit PayoutTransferred(game, receiver, address(0), amount);
75:     }

['164']

164:     function initiateGameLiquidityPoolConnectionRequest(
165:         address game,
166:         address currency,
167:         address liquidityPool
168:     ) external onlyOwner {
169:         address liquidityPoolCurrency = IERC4626(liquidityPool).asset(); // <= FOUND
170: 
171:         if ((currency != liquidityPoolCurrency) && !(currency == address(0) && liquidityPoolCurrency == WETH)) {
172:             revert GameConfigurationManager__GameLiquidityPoolCurrencyMismatch();
173:         }
174: 
175:         if (IERC4626(liquidityPool).totalSupply() == 0) { // <= FOUND
176:             gameLiquidityPool[game][currency] = liquidityPool;
177:             kellyFractionBasisPoints[game] = 10_000;
178:             emit GameAndLiquidityPoolConnected(game, currency, liquidityPool);
179:         } else {
180:             bytes32 requestId = keccak256(abi.encodePacked(game, currency, liquidityPool));
181:             gameLiquidityPoolConnectionRequests[requestId] = block.timestamp;
182:             emit GameAndLiquidityPoolConnectionRequestInitiated(game, currency, liquidityPool);
183:         }
184:     }

['228']

228:     function addLiquidityPool(address liquidityPool) external onlyOwner { // <= FOUND
229:         address token = ERC4626(liquidityPool).asset(); // <= FOUND
230:         if (liquidityPools[token] != address(0)) {
231:             revert LiquidityPoolRouter__TokenAlreadyHasLiquidityPool();
232:         }
233:         liquidityPools[token] = liquidityPool;
234:         emit LiquidityPoolRouter__LiquidityPoolAdded(token, liquidityPool);
235:     }

['284']

284:     function depositETH(uint256 amount) external payable nonReentrant whenNotPaused { // <= FOUND
285:         address liquidityPool = _getLiquidityPoolOrRevert(WETH);
286: 
287:         if (amount + finalizationParams.finalizationIncentive != msg.value) {
288:             revert LiquidityPoolRouter__FinalizationIncentiveNotPaid();
289:         }
290: 
291:         uint256 depositFee = (amount * DEPOSIT_FEE_BASIS_POINTS) / 10_000;
292:         amount -= depositFee;
293: 
294:         _validateDepositAmount(liquidityPool, amount);
295: 
296:         if (deposits[msg.sender].amount != 0) {
297:             revert LiquidityPoolRouter__OngoingDeposit();
298:         }
299: 
300:         _transferETHAndWrapIfFailWithGasLimit(WETH, owner, depositFee, gasleft());
301: 
302:         uint256 expectedShares = ERC4626(liquidityPool).previewDeposit(amount); // <= FOUND
303: 
304:         deposits[msg.sender] = Deposit(
305:             liquidityPool,
306:             amount,
307:             expectedShares,
308:             block.timestamp,
309:             finalizationParams.finalizationIncentive
310:         );
311:         pendingDeposits[liquidityPool] += amount;
312: 
313:         emit LiquidityPoolRouter__DepositInitialized(
314:             msg.sender,
315:             liquidityPool,
316:             amount + depositFee,
317:             expectedShares,
318:             finalizationParams.finalizationIncentive
319:         );
320:     }

['329']

329:     function deposit(address token, uint256 amount) external payable nonReentrant whenNotPaused { // <= FOUND
330:         address liquidityPool = _getLiquidityPoolOrRevert(token);
331: 
332:         _validateFinalizationIncentivePayment();
333: 
334:         uint256 depositFee = (amount * DEPOSIT_FEE_BASIS_POINTS) / 10_000;
335:         amount -= depositFee;
336: 
337:         _validateDepositAmount(liquidityPool, amount);
338: 
339:         if (deposits[msg.sender].amount != 0) {
340:             revert LiquidityPoolRouter__OngoingDeposit();
341:         }
342: 
343:         TRANSFER_MANAGER.transferERC20(token, msg.sender, address(this), amount);
344:         TRANSFER_MANAGER.transferERC20(token, msg.sender, owner, depositFee);
345: 
346:         uint256 expectedShares = ERC4626(liquidityPool).previewDeposit(amount); // <= FOUND
347: 
348:         deposits[msg.sender] = Deposit(
349:             liquidityPool,
350:             amount,
351:             expectedShares,
352:             block.timestamp,
353:             finalizationParams.finalizationIncentive
354:         );
355:         pendingDeposits[liquidityPool] += amount;
356: 
357:         emit LiquidityPoolRouter__DepositInitialized(
358:             msg.sender,
359:             liquidityPool,
360:             amount + depositFee,
361:             expectedShares,
362:             finalizationParams.finalizationIncentive
363:         );
364:     }

['374']

374:     function finalizeDeposit(address depositor) external nonReentrant { // <= FOUND
375:         uint256 amount = deposits[depositor].amount;
376:         if (amount == 0) {
377:             revert LiquidityPoolRouter__NoOngoingDeposit();
378:         }
379: 
380:         uint256 initializedAt = deposits[depositor].initializedAt;
381:         _validateTimelockIsOver(initializedAt);
382:         _validateFinalizationIsOpenForAll(depositor, initializedAt);
383: 
384:         address payable liquidityPool = payable(deposits[depositor].liquidityPool);
385:         address token = ERC4626(liquidityPool).asset(); // <= FOUND
386:         uint256 expectedShares = deposits[depositor].expectedShares;
387:         uint256 actualShares = ERC4626(liquidityPool).previewDeposit(amount); // <= FOUND
388:         uint256 incentive = deposits[depositor].finalizationIncentive;
389: 
390:         deposits[depositor] = Deposit(address(0), 0, 0, 0, 0);
391:         pendingDeposits[liquidityPool] -= amount;
392: 
393:         _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, incentive, 2_300);
394: 
395:         uint256 sharesMinted;
396:         uint256 amountRequired;
397:         if (expectedShares >= actualShares) {
398:             amountRequired = amount;
399:             sharesMinted = _deposit(token, liquidityPool, amountRequired, depositor);
400:         } else {
401:             amountRequired = ERC4626(liquidityPool).previewMint(expectedShares); // <= FOUND
402:             sharesMinted = _deposit(token, liquidityPool, amountRequired, depositor);
403:             if (token == WETH) {
404:                 _transferETHAndWrapIfFailWithGasLimit(WETH, liquidityPool, amount - amountRequired, gasleft());
405:             } else {
406:                 _executeERC20DirectTransfer(token, liquidityPool, amount - amountRequired);
407:             }
408:         }
409: 
410:         emit LiquidityPoolRouter__DepositFinalized(msg.sender, depositor, liquidityPool, amountRequired, sharesMinted);
411:     }

['420']

420:     function redeem(address token, uint256 amount) external payable nonReentrant { // <= FOUND
421:         address liquidityPool = _getLiquidityPoolOrRevert(token);
422: 
423:         _validateFinalizationIncentivePayment();
424: 
425:         if (redemptions[msg.sender].shares != 0) {
426:             revert LiquidityPoolRouter__OngoingRedemption();
427:         }
428: 
429:         TRANSFER_MANAGER.transferERC20(liquidityPool, msg.sender, address(this), amount);
430: 
431:         uint256 expectedAssets = ERC4626(liquidityPool).previewRedeem(amount); // <= FOUND
432: 
433:         redemptions[msg.sender] = Redemption(
434:             liquidityPool,
435:             amount,
436:             expectedAssets,
437:             block.timestamp,
438:             finalizationParams.finalizationIncentive
439:         );
440: 
441:         emit LiquidityPoolRouter__RedemptionInitialized(
442:             msg.sender,
443:             liquidityPool,
444:             amount,
445:             expectedAssets,
446:             finalizationParams.finalizationIncentive
447:         );
448:     }

['458']

458:     function finalizeRedemption(address redeemer) external nonReentrant { // <= FOUND
459:         uint256 amount = redemptions[redeemer].shares;
460:         if (amount == 0) {
461:             revert LiquidityPoolRouter__NoOngoingRedemption();
462:         }
463: 
464:         uint256 initializedAt = redemptions[redeemer].initializedAt;
465:         _validateTimelockIsOver(initializedAt);
466:         _validateFinalizationIsOpenForAll(redeemer, initializedAt);
467: 
468:         address payable liquidityPool = payable(redemptions[redeemer].liquidityPool);
469:         address token = ERC4626(liquidityPool).asset(); // <= FOUND
470:         uint256 expectedAssets = redemptions[redeemer].expectedAssets;
471:         uint256 incentive = redemptions[redeemer].finalizationIncentive;
472: 
473:         redemptions[redeemer] = Redemption(address(0), 0, 0, 0, 0);
474: 
475:         uint256 assetsRedeemed = LiquidityPool(liquidityPool).redeem(amount, address(this), address(this)); // <= FOUND
476: 
477:         _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, incentive, 2_300);
478: 
479:         if (expectedAssets >= assetsRedeemed) {
480:             _transferAssetsRedeemed(token, redeemer, assetsRedeemed);
481:         } else {
482:             _transferAssetsRedeemed(token, redeemer, expectedAssets);
483:             _executeERC20DirectTransfer(token, liquidityPool, assetsRedeemed - expectedAssets);
484:             assetsRedeemed = expectedAssets;
485:         }
486: 
487:         emit LiquidityPoolRouter__RedemptionFinalized(msg.sender, redeemer, liquidityPool, amount, assetsRedeemed);
488:     }

[Low-5] Do not use block values to generate randomness

Num of instances: 5

Findings

Click to show findings

['208']

208:         games[msg.sender].params = Game__GameParams({
209:             blockNumber: uint40(block.number), // <= FOUND
210:             numberOfRounds: numberOfRounds,
211:             playAmountPerRound: playAmountPerRound,
212:             currency: currency,
213:             stopGain: 0,
214:             stopLoss: 0,
215:             randomnessRequestedAt: uint40(block.timestamp), // <= FOUND
216:             vrfFee: vrfFee
217:         });

['278']

278:         games[msg.sender].params.randomnessRequestedAt = uint40(block.timestamp); // <= FOUND

['108']

108:         games[msg.sender] = Flipper__Game({
109:             params: Game__GameParams({
110:                 blockNumber: uint40(block.number), // <= FOUND
111:                 numberOfRounds: numberOfRounds,
112:                 playAmountPerRound: playAmountPerRound,
113:                 currency: currency,
114:                 stopGain: stopGain,
115:                 stopLoss: stopLoss,
116:                 randomnessRequestedAt: uint40(block.timestamp), // <= FOUND
117:                 vrfFee: vrfFee
118:             }),

['189']

189:         games[msg.sender] = LaserBlast__Game({
190:             params: Game__GameParams({
191:                 blockNumber: uint40(block.number), // <= FOUND
192:                 numberOfRounds: numberOfRounds,
193:                 playAmountPerRound: playAmountPerRound,
194:                 currency: currency,
195:                 stopGain: stopGain,
196:                 stopLoss: stopLoss,
197:                 randomnessRequestedAt: uint40(block.timestamp), // <= FOUND
198:                 vrfFee: vrfFee
199:             }),

['129']

129:         games[msg.sender] = Quantum__Game({
130:             params: Game__GameParams({
131:                 blockNumber: uint40(block.number), // <= FOUND
132:                 numberOfRounds: numberOfRounds,
133:                 playAmountPerRound: playAmountPerRound,
134:                 currency: currency,
135:                 stopGain: stopGain,
136:                 stopLoss: stopLoss,
137:                 randomnessRequestedAt: uint40(block.timestamp), // <= FOUND
138:                 vrfFee: vrfFee
139:             }),

[Low-6] Contract contains payable functions but no withdraw/sweep function

Resolution

In smart contract development, particularly for Ethereum, having payable functions without a corresponding withdraw or sweep function can lead to potential issues. Payable functions allow the contract to receive Ether, but without a mechanism to withdraw these funds, the Ether can become locked within the contract indefinitely. This situation might be intentional in some cases (like a burn function), but generally, it’s a design oversight. A withdraw or sweep function is necessary to transfer Ether out of the contract to a specific address, typically the owner's or a designated recipient. Without this, the contract lacks flexibility in managing its funds, potentially leading to lost or inaccessible Ether.

Num of instances: 6

Findings

Click to show findings

['13']

13: contract DontFallIn is Game 

['13']

13: contract Flipper is Game 

['13']

13: contract LaserBlast is Game 

['26']

26: contract LiquidityPoolRouter is OwnableTwoSteps, LowLevelWETH, LowLevelERC20Transfer, ReentrancyGuard, Pausable 

['13']

13: contract Quantum is Game 

['18']

18: contract EthLiquidityPool is LiquidityPool, LowLevelWETH 

[Low-7] Using block.number is not fully L2 compatible

Resolution

Using block.number can break compatibility with some L2s such as Optimism whos time between blocks differ from Ethereum and isn't constant. Consider using block.timestamp to prevent compatibility issues

Num of instances: 8

Findings

Click to show findings

['208']

208:         games[msg.sender].params = Game__GameParams({
209:             blockNumber: uint40(block.number), // <= FOUND
210:             numberOfRounds: numberOfRounds,
211:             playAmountPerRound: playAmountPerRound,
212:             currency: currency,
213:             stopGain: 0,
214:             stopLoss: 0,
215:             randomnessRequestedAt: uint40(block.timestamp),
216:             vrfFee: vrfFee
217:         });

['222']

222:         emit DontFallIn__GameCreated(
223:             block.number, // <= FOUND
224:             msg.sender,
225:             playAmountPerRound,
226:             currency,
227:             lavasCount,
228:             selectedTiles,
229:             cashoutIfWon
230:         );

['108']

108:         games[msg.sender] = Flipper__Game({
109:             params: Game__GameParams({
110:                 blockNumber: uint40(block.number), // <= FOUND
111:                 numberOfRounds: numberOfRounds,
112:                 playAmountPerRound: playAmountPerRound,
113:                 currency: currency,
114:                 stopGain: stopGain,
115:                 stopLoss: stopLoss,
116:                 randomnessRequestedAt: uint40(block.timestamp),
117:                 vrfFee: vrfFee
118:             }),

['122']

122:         emit Flipper__GameCreated(
123:             block.number, // <= FOUND
124:             msg.sender,
125:             numberOfRounds,
126:             playAmountPerRound,
127:             currency,
128:             stopGain,
129:             stopLoss,
130:             isGold
131:         );

['189']

189:         games[msg.sender] = LaserBlast__Game({
190:             params: Game__GameParams({
191:                 blockNumber: uint40(block.number), // <= FOUND
192:                 numberOfRounds: numberOfRounds,
193:                 playAmountPerRound: playAmountPerRound,
194:                 currency: currency,
195:                 stopGain: stopGain,
196:                 stopLoss: stopLoss,
197:                 randomnessRequestedAt: uint40(block.timestamp),
198:                 vrfFee: vrfFee
199:             }),

['204']

204:         emit LaserBlast__GameCreated(
205:             block.number, // <= FOUND
206:             msg.sender,
207:             numberOfRounds,
208:             playAmountPerRound,
209:             currency,
210:             stopGain,
211:             stopLoss,
212:             riskLevel,
213:             rowCount
214:         );

['129']

129:         games[msg.sender] = Quantum__Game({
130:             params: Game__GameParams({
131:                 blockNumber: uint40(block.number), // <= FOUND
132:                 numberOfRounds: numberOfRounds,
133:                 playAmountPerRound: playAmountPerRound,
134:                 currency: currency,
135:                 stopGain: stopGain,
136:                 stopLoss: stopLoss,
137:                 randomnessRequestedAt: uint40(block.timestamp),
138:                 vrfFee: vrfFee
139:             }),

['144']

144:         emit Quantum__GameCreated(
145:             block.number, // <= FOUND
146:             msg.sender,
147:             numberOfRounds,
148:             playAmountPerRound,
149:             currency,
150:             stopGain,
151:             stopLoss,
152:             isAbove,
153:             multiplier
154:         );

[Low-8] Empty receive functions can cause gas issues

Resolution

In Solidity, functions that receive Ether without corresponding functionality to utilize or withdraw these funds can inadvertently lead to a permanent loss of value. This is because if someone sends Ether to the contract, they may be unable to retrieve it. To avoid this, functions receiving Ether should be accompanied by additional methods that process or allow the withdrawal of these funds. If the intent is to use the received Ether, it should trigger a separate function; if not, it should revert the transaction (for instance, via require(msg.sender == address(weth))). Access control checks can also prevent unintended Ether transfers, despite the slight gas cost they entail. If concerns over gas costs persist, at minimum, include a rescue function to recover unused Ether. Missteps in handling Ether in smart contracts can lead to irreversible financial losses, hence these precautions are crucial.

Num of instances: 1

Findings

Click to show findings

['506']

506:     receive() external payable {}

[Low-9] Loss of precision

Resolution

Dividing by large numbers in Solidity can cause a loss of precision due to the language's inherent integer division behavior. Solidity does not support floating-point arithmetic, and as a result, division between integers yields an integer result, truncating any fractional part. When dividing by a large number, the resulting value may become significantly smaller, leading to a loss of precision, as the fractional part is discarded.

Num of instances: 5

Findings

Click to show findings

['294']

294:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
295:         address player = randomnessRequests[requestId];
296: 
297:         if (player != address(0)) { // <= FOUND
298:             LaserBlast__Game storage game = games[player];
299:             if (_hasLiquidityPool(game.params.currency)) { // <= FOUND
300:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) { // <= FOUND
301:                     randomnessRequests[requestId] = address(0);
302: 
303:                     RunningGameState memory runningGameState;
304:                     runningGameState.payouts = new uint256[](game.params.numberOfRounds);
305:                     runningGameState.randomWord = randomWords[0];
306:                     uint256[] memory results = new uint256[](game.params.numberOfRounds);
307: 
308:                     IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
309:                         address(this)
310:                     );
311: 
312:                     uint256 adjustedLiquidityPoolFeeBasisPoints = _applyMultiplierToLiquidityPoolFeeBasisPoints(
313:                         feeSplit.liquidityPoolFeeBasisPoints,
314:                         game.riskLevel
315:                     );
316: 
317:                     Fee memory fee;
318:                     uint256 playAmountPerRound = game.params.playAmountPerRound;
319: 
320:                     for (
321:                         ;
322:                         runningGameState.numberOfRoundsPlayed < game.params.numberOfRounds;
323:                         ++runningGameState.numberOfRoundsPlayed
324:                     ) {
325:                         if ( // <= FOUND
326:                             _stopGainOrStopLossHit(
327:                                 game.params.stopGain,
328:                                 game.params.stopLoss,
329:                                 runningGameState.netAmount
330:                             )
331:                         ) {
332:                             break;
333:                         }
334: 
335:                         (uint256 result, uint256 multiplier, uint256 randomWordForNextRound) = _dropTheBall(
336:                             game.riskLevel,
337:                             game.rowCount,
338:                             runningGameState.randomWord
339:                         );
340: 
341:                         uint256 protocolFee = (playAmountPerRound * multiplier * feeSplit.protocolFeeBasisPoints) / 1e8;
342:                         uint256 liquidityPoolFee = (playAmountPerRound *
343:                             multiplier *
344:                             adjustedLiquidityPoolFeeBasisPoints) / 1e8;
345: 
346:                         runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
347:                             (playAmountPerRound * multiplier) /
348:                             10_000 -
349:                             protocolFee -
350:                             liquidityPoolFee;
351:                         runningGameState.payout += runningGameState.payouts[runningGameState.numberOfRoundsPlayed];
352:                         runningGameState.netAmount += (int256(
353:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed]
354:                         ) - int256(playAmountPerRound));
355:                         fee.protocolFee += protocolFee;
356:                         fee.liquidityPoolFee += liquidityPoolFee;
357:                         results[runningGameState.numberOfRoundsPlayed] = result;
358: 
359:                         runningGameState.randomWord = randomWordForNextRound;
360:                     }
361: 
362:                     _handlePayout(
363:                         player,
364:                         game.params,
365:                         runningGameState.numberOfRoundsPlayed,
366:                         runningGameState.payout,
367:                         fee.protocolFee
368:                     );
369:                     _transferVrfFee(game.params.vrfFee);
370: 
371:                     emit LaserBlast__GameCompleted(
372:                         game.params.blockNumber,
373:                         player,
374:                         results,
375:                         runningGameState.payouts,
376:                         runningGameState.numberOfRoundsPlayed,
377:                         fee.protocolFee,
378:                         fee.liquidityPoolFee
379:                     );
380: 
381:                     _deleteGame(player);
382:                 }
383:             }
384:         }
385:     }

['238']

238:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
239:         address player = randomnessRequests[requestId];
240:         if (player != address(0)) { // <= FOUND
241:             Quantum__Game storage game = games[player];
242:             if (_hasLiquidityPool(game.params.currency)) { // <= FOUND
243:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) { // <= FOUND
244:                     randomnessRequests[requestId] = address(0);
245: 
246:                     RunningGameState memory runningGameState;
247:                     runningGameState.payouts = new uint256[](game.params.numberOfRounds);
248:                     runningGameState.randomWord = randomWords[0];
249:                     uint256[] memory results = new uint256[](game.params.numberOfRounds);
250: 
251:                     IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
252:                         address(this)
253:                     );
254:                     Fee memory fee;
255: 
256:                     for (
257:                         ;
258:                         runningGameState.numberOfRoundsPlayed < game.params.numberOfRounds;
259:                         ++runningGameState.numberOfRoundsPlayed
260:                     ) {
261:                         if ( // <= FOUND
262:                             _stopGainOrStopLossHit(
263:                                 game.params.stopGain,
264:                                 game.params.stopLoss,
265:                                 runningGameState.netAmount
266:                             )
267:                         ) {
268:                             break;
269:                         }
270: 
271:                         results[runningGameState.numberOfRoundsPlayed] = runningGameState.randomWord % TOTAL_OUTCOMES;
272:                         if ( // <= FOUND
273:                             (game.isAbove &&
274:                                 results[runningGameState.numberOfRoundsPlayed] >=
275:                                 defineBoundary(calculateWinProbability(game.multiplier))) ||
276:                             (!game.isAbove &&
277:                                 results[runningGameState.numberOfRoundsPlayed] <
278:                                 calculateWinProbability(game.multiplier))
279:                         ) {
280:                             uint256 protocolFee = (game.multiplier *
281:                                 game.params.playAmountPerRound *
282:                                 feeSplit.protocolFeeBasisPoints) / 1e8;
283:                             uint256 liquidityPoolFee = (game.multiplier *
284:                                 game.params.playAmountPerRound *
285:                                 feeSplit.liquidityPoolFeeBasisPoints) / 1e8;
286: 
287:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
288:                                 ((game.multiplier * game.params.playAmountPerRound) / 10_000) -
289:                                 protocolFee -
290:                                 liquidityPoolFee;
291:                             runningGameState.payout += runningGameState.payouts[runningGameState.numberOfRoundsPlayed];
292:                             runningGameState.netAmount += int256(
293:                                 runningGameState.payouts[runningGameState.numberOfRoundsPlayed] -
294:                                     game.params.playAmountPerRound
295:                             );
296:                             fee.protocolFee += protocolFee;
297:                             fee.liquidityPoolFee += liquidityPoolFee;
298:                         } else {
299:                             runningGameState.netAmount -= int256(game.params.playAmountPerRound);
300:                         }
301:                         runningGameState.randomWord = uint256(keccak256(abi.encode(runningGameState.randomWord)));
302:                     }
303: 
304:                     _handlePayout(
305:                         player,
306:                         game.params,
307:                         runningGameState.numberOfRoundsPlayed,
308:                         runningGameState.payout,
309:                         fee.protocolFee
310:                     );
311:                     _transferVrfFee(game.params.vrfFee);
312: 
313:                     emit Quantum__GameCompleted(
314:                         game.params.blockNumber,
315:                         player,
316:                         results,
317:                         runningGameState.payouts,
318:                         runningGameState.numberOfRoundsPlayed,
319:                         fee.protocolFee,
320:                         fee.liquidityPoolFee
321:                     );
322: 
323:                     _deleteGame(player);
324:                 }
325:             }
326:         }
327:     }

['585']

585:     function _getMultiplier(uint256 winProbability) private pure returns (uint256 multiplier) { // <= FOUND
586:         multiplier = (10_000 * 1e18) / winProbability; // <= FOUND
587:     }

['188']

188:     function kellyFraction() public view returns (uint256) { // <= FOUND
189:         uint256 multiplier = (_liquidityProviderAdjustedReturn() * 10_000) / 5_000 - 10_000;
190: 
191:         return (5_000 * KELLY_FRACTION_SCALER) / multiplier - (5_000 * KELLY_FRACTION_SCALER) / 10_000; // <= FOUND
192:     }

['333']

333:     function calculateWinProbability(uint256 multiplier) public pure returns (uint256 winProbability) { // <= FOUND
334:         winProbability = 100_000_000_000 / multiplier; // <= FOUND
335:     }

[Low-10] Missing zero address check in constructor

Resolution

In Solidity, constructors often take address parameters to initialize important components of a contract, such as owner or linked contracts. However, without a check, there's a risk that an address parameter could be mistakenly set to the zero address (0x0). This could occur due to a mistake or oversight during contract deployment. A zero address in a crucial role can cause serious issues, as it cannot perform actions like a normal address, and any funds sent to it are irretrievable. Therefore, it's crucial to include a zero address check in constructors to prevent such potential problems. If a zero address is detected, the constructor should revert the transaction.

Num of instances: 8

Findings

Click to show findings

['122']

122:     constructor(
123:         address _gameConfigurationManager, // <= FOUND
124:         address _transferManager, // <= FOUND
125:         address _weth, // <= FOUND
126:         address _vrfCoordinator, // <= FOUND
127:         address _blast, // <= FOUND
128:         address _usdb, // <= FOUND
129:         address _owner, // <= FOUND
130:         uint256[25] memory _maximumRevealableTiles
131:     ) Game(_gameConfigurationManager, _transferManager, _weth, _vrfCoordinator, _blast, _usdb, _owner) {
132:         uint256 maximumRevealableTilesLength = _maximumRevealableTiles.length;
133:         for (uint256 i; i < maximumRevealableTilesLength; ++i) {
134:             maximumRevealableTiles[i] = _maximumRevealableTiles[i];
135:         }
136: 
137:         for (uint256 lavasCount = 1; lavasCount < GRID_SIZE; ++lavasCount) {
138:             uint256 maximumRevealableTilesCount = _maximumRevealableTiles[lavasCount];
139:             for (
140:                 uint256 revealedTilesCount = 1;
141:                 revealedTilesCount <= maximumRevealableTilesCount;
142:                 ++revealedTilesCount
143:             ) {
144:                 uint256 numerator = 1;
145:                 uint256 denominator = 1;
146:                 for (
147:                     uint256 revealedTilesCountSoFar;
148:                     revealedTilesCountSoFar < revealedTilesCount;
149:                     ++revealedTilesCountSoFar
150:                 ) {
151:                     numerator *= (GRID_SIZE - lavasCount - revealedTilesCountSoFar);
152:                     denominator *= (GRID_SIZE - revealedTilesCountSoFar);
153:                 }
154:                 winProbabilities[lavasCount][revealedTilesCount] = (numerator * 1e18) / denominator;
155:             }
156:         }
157:     }

['29']

29:     constructor(
30:         string memory _name,
31:         string memory _symbol,
32:         address _owner, // <= FOUND
33:         address _asset, // <= FOUND
34:         bool _isUSDB,
35:         address _gameConfigurationManager, // <= FOUND
36:         address _liquidityPoolRouter, // <= FOUND
37:         address _blast, // <= FOUND
38:         address _blastPoints, // <= FOUND
39:         address _blastPointsOperator // <= FOUND
40:     )
41:         LiquidityPool(
42:             _name,
43:             _symbol,
44:             _owner,
45:             _asset,
46:             _gameConfigurationManager,
47:             _liquidityPoolRouter,
48:             _blast,
49:             _blastPoints,
50:             _blastPointsOperator
51:         )
52:     {
53:         if (_isUSDB) {
54:             IERC20Rebasing(_asset).configure(YieldMode.CLAIMABLE);
55:         }
56:     }

['28']

28:     constructor(
29:         address _owner, // <= FOUND
30:         address _weth, // <= FOUND
31:         address _gameConfigurationManager, // <= FOUND
32:         address _liquidityPoolRouter, // <= FOUND
33:         address _blast, // <= FOUND
34:         address _blastPoints, // <= FOUND
35:         address _blastPointsOperator // <= FOUND
36:     )
37:         LiquidityPool(
38:             "YOLO Games ETH",
39:             "yETH",
40:             _owner,
41:             _weth,
42:             _gameConfigurationManager,
43:             _liquidityPoolRouter,
44:             _blast,
45:             _blastPoints,
46:             _blastPointsOperator
47:         )
48:     {
49:         IERC20Rebasing(_weth).configure(YieldMode.CLAIMABLE);
50:     }

['115']

115:     constructor(
116:         address _gameConfigurationManager, // <= FOUND
117:         address _transferManager, // <= FOUND
118:         address _weth, // <= FOUND
119:         address _vrfCoordinator, // <= FOUND
120:         address _blast, // <= FOUND
121:         address _usdb, // <= FOUND
122:         address _owner // <= FOUND
123:     ) VRFConsumerBaseV2(_vrfCoordinator) OwnableTwoSteps(_owner) {
124:         GAME_CONFIGURATION_MANAGER = IGameConfigurationManager(_gameConfigurationManager);
125:         TRANSFER_MANAGER = ITransferManager(_transferManager);
126:         WETH = _weth;
127:         USDB = _usdb;
128: 
129:         (address coordinator, , , , , ) = GAME_CONFIGURATION_MANAGER.vrfParameters(); // <= FOUND
130:         if (coordinator != _vrfCoordinator) {
131:             revert Game__WrongVrfCoordinator();
132:         }
133: 
134:         IBlast(_blast).configure(IBlast__YieldMode.CLAIMABLE, GasMode.CLAIMABLE, _owner);
135:         IERC20Rebasing(_usdb).configure(IERC20Rebasing__YieldMode.CLAIMABLE);
136:     }

['126']

126:     constructor(
127:         address _owner, // <= FOUND
128:         address _weth, // <= FOUND
129:         address _blast, // <= FOUND
130:         address _vrfFeeRecipient, // <= FOUND
131:         address _protocolFeeRecipient, // <= FOUND
132:         address _vrfCoordinator, // <= FOUND
133:         bytes32 _keyHash,
134:         uint64 _subscriptionId
135:     ) OwnableTwoSteps(_owner) {
136:         WETH = _weth;
137: 
138:         vrfFeeRecipient = _vrfFeeRecipient;
139:         emit VrfFeeRecipientUpdated(_vrfFeeRecipient);
140: 
141:         protocolFeeRecipient = _protocolFeeRecipient;
142:         emit ProtocolFeeRecipientUpdated(_protocolFeeRecipient);
143: 
144:         VrfParameters memory _vrfParameters = VrfParameters({
145:             coordinator: _vrfCoordinator,
146:             subscriptionId: _subscriptionId,
147:             callbackGasLimit: 2_500_000,
148:             minimumRequestConfirmations: 3,
149:             vrfFee: 0.0003 ether,
150:             keyHash: _keyHash
151:         });
152:         _setVrfParameters(_vrfParameters);
153: 
154:         IBlast(_blast).configure(YieldMode.CLAIMABLE, GasMode.CLAIMABLE, _owner);
155:     }

['95']

95:     constructor(
96:         address _gameConfigurationManager, // <= FOUND
97:         address _transferManager, // <= FOUND
98:         address _weth, // <= FOUND
99:         address _vrfCoordinator, // <= FOUND
100:         address _blast, // <= FOUND
101:         address _usdb, // <= FOUND
102:         address _owner // <= FOUND
103:     ) Game(_gameConfigurationManager, _transferManager, _weth, _vrfCoordinator, _blast, _usdb, _owner) {
104:         kellyFractions[0] = [41300, 25900, 31400, 29500, 63700, 49000, 49700, 47300, 28800];
105:         kellyFractions[1] = [6320, 9480, 11300, 9920, 10920, 10280, 9460, 8490, 7350];
106:         kellyFractions[2] = [4049, 2689, 1563, 1599, 1247, 869, 547, 634, 369];
107:     }

['53']

53:     constructor(
54:         string memory _name,
55:         string memory _symbol,
56:         address _owner, // <= FOUND
57:         address _asset, // <= FOUND
58:         address _gameConfigurationManager, // <= FOUND
59:         address _liquidityPoolRouter, // <= FOUND
60:         address _blast, // <= FOUND
61:         address _blastPoints, // <= FOUND
62:         address _blastPointsOperator // <= FOUND
63:     ) ERC20(_name, _symbol) ERC4626(IERC20(_asset)) OwnableTwoSteps(_owner) {
64:         GAME_CONFIGURATION_MANAGER = _gameConfigurationManager;
65:         LIQUIDITY_POOL_ROUTER = _liquidityPoolRouter;
66: 
67:         IBlast(_blast).configure(YieldMode.CLAIMABLE, GasMode.CLAIMABLE, _owner);
68:         IBlastPoints(_blastPoints).configurePointsOperator(_blastPointsOperator);
69:     }

['202']

202:     constructor(
203:         address _owner, // <= FOUND
204:         address _weth, // <= FOUND
205:         address _usdb, // <= FOUND
206:         address _transferManager, // <= FOUND
207:         address _blast, // <= FOUND
208:         address _blastPoints, // <= FOUND
209:         address _blastPointsOperator // <= FOUND
210:     ) OwnableTwoSteps(_owner) {
211:         WETH = _weth;
212:         USDB = _usdb;
213:         TRANSFER_MANAGER = ITransferManager(_transferManager);
214: 
215:         _setFinalizationParams(10 seconds, 5 minutes, 0.0003 ether);
216: 
217:         IBlast(_blast).configure(IBlast__YieldMode.CLAIMABLE, IBlast__GasMode.CLAIMABLE, _owner);
218:         IERC20Rebasing(_weth).configure(IERC20Rebasing__YieldMode.CLAIMABLE);
219:         IERC20Rebasing(_usdb).configure(IERC20Rebasing__YieldMode.CLAIMABLE);
220:         IBlastPoints(_blastPoints).configurePointsOperator(_blastPointsOperator);
221:     }

[Low-11] Using zero as a parameter

Resolution

Taking 0 as a valid argument in Solidity without checks can lead to severe security issues. A historical example is the infamous 0x0 address bug where numerous tokens were lost. This happens because '0' can be interpreted as an uninitialized address, leading to transfers to the '0x0' address, effectively burning tokens. Moreover, 0 as a denominator in division operations would cause a runtime exception. It's also often indicative of a logical error in the caller's code. It's important to always validate input and handle edge cases like 0 appropriately. Use require() statements to enforce conditions and provide clear error messages to facilitate debugging and safer code.

Num of instances: 6

Findings

Click to show findings

['459']

459:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
460:         address player = randomnessRequests[requestId];
461:         if (player != address(0)) {
462:             DontFallIn__Game storage game = games[player];
463:             if (_hasLiquidityPool(game.params.currency)) {
464:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) {
465:                     randomnessRequests[requestId] = address(0);
466: 
467:                     RunningGameState memory runningGameState;
468:                     runningGameState.randomWord = randomWords[0];
469: 
470:                     uint256 lavas;
471:                     (uint256 selectedTilesCount, uint256[] memory indices) = _nonZeroTilesCount(game.selectedTiles);
472:                     uint32 grid = game.grid;
473:                     (uint256 revealedTilesCount, ) = _nonZeroTilesCount(grid);
474:                     for (uint256 i; i < selectedTilesCount; ++i) {
475:                         if (
476:                             runningGameState.randomWord % 10_000 <
477:                             (game.lavasCount * 10_000) / (GRID_SIZE - revealedTilesCount)
478:                         ) {
479:                             lavas |= 1 << indices[i];
480:                         } else {
481:                             grid |= uint32(1 << indices[i]);
482:                         }
483: 
484:                         unchecked {
485:                             ++revealedTilesCount;
486:                         }
487: 
488:                         runningGameState.randomWord = uint256(keccak256(abi.encode(runningGameState.randomWord)));
489:                     }
490: 
491:                     _transferVrfFee(game.params.vrfFee);
492: 
493:                     if (lavas == 0) {
494:                         IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
495:                             address(this)
496:                         );
497: 
498:                         uint256 multiplier = _getMultiplier(winProbabilities[game.lavasCount][revealedTilesCount]);
499:                         if (game.cashoutIfWon) {
500:                             Fee memory fee = Fee({
501:                                 protocolFee: (game.params.playAmountPerRound *
502:                                     multiplier *
503:                                     feeSplit.protocolFeeBasisPoints) / 1e8,
504:                                 liquidityPoolFee: (game.params.playAmountPerRound *
505:                                     multiplier *
506:                                     feeSplit.liquidityPoolFeeBasisPoints) / 1e8
507:                             });
508:                             runningGameState.payout =
509:                                 (game.params.playAmountPerRound * multiplier) /
510:                                 10_000 -
511:                                 fee.protocolFee -
512:                                 fee.liquidityPoolFee;
513:                             _handlePayout(player, game.params, 1, runningGameState.payout, fee.protocolFee);
514:                             emit DontFallIn__GameWon(
515:                                 game.params.blockNumber,
516:                                 player,
517:                                 game.selectedTiles,
518:                                 runningGameState.payout,
519:                                 multiplier,
520:                                 fee.protocolFee,
521:                                 fee.liquidityPoolFee
522:                             );
523:                             _deleteGame(player);
524:                         } else {
525:                             game.params.randomnessRequestedAt = 0;
526:                             game.params.vrfFee = 0;
527:                             game.grid = grid;
528:                             game.multiplier = uint176(multiplier);
529: 
530:                             emit DontFallIn__GamePlayed(
531:                                 game.params.blockNumber,
532:                                 player,
533:                                 game.selectedTiles,
534:                                 game.multiplier
535:                             );
536:                         }
537:                     } else {
538:                         _handlePayout(player, game.params, 1, 0, 0); // <= FOUND
539:                         emit DontFallIn__GameLost(game.params.blockNumber, player, game.selectedTiles, lavas);
540:                         _deleteGame(player);
541:                     }
542:                 }
543:             }
544:         }
545:     }

['619']

619:     function _deleteGame(address player) private { // <= FOUND
620:         games[player] = DontFallIn__Game({
621:             params: Game__GameParams({
622:                 blockNumber: 0,
623:                 numberOfRounds: 0,
624:                 playAmountPerRound: 0,
625:                 currency: address(0),
626:                 stopGain: 0,
627:                 stopLoss: 0,
628:                 randomnessRequestedAt: 0,
629:                 vrfFee: 0
630:             }),
631:             grid: 0, // <= FOUND
632:             lavasCount: 0,
633:             selectedTiles: 0,
634:             cashoutIfWon: false,
635:             multiplier: 0
636:         });
637:     }

['421']

421:     function _deleteGame(address player) private { // <= FOUND
422:         games[player] = LaserBlast__Game({
423:             params: Game__GameParams({
424:                 blockNumber: 0,
425:                 numberOfRounds: 0,
426:                 playAmountPerRound: 0,
427:                 currency: address(0),
428:                 stopGain: 0,
429:                 stopLoss: 0,
430:                 randomnessRequestedAt: 0,
431:                 vrfFee: 0
432:             }),
433:             riskLevel: 0, // <= FOUND
434:             rowCount: 0
435:         });
436:     }

['462']

462:     function _validateMultiplierIsSet(uint256 riskLevel, uint256 rowCount) private view { // <= FOUND
463:         if (multipliers[_multiplierKey(riskLevel, rowCount, 0)] == 0) { // <= FOUND
464:             revert Game__ZeroMultiplier();
465:         }
466:     }

['374']

374:     function finalizeDeposit(address depositor) external nonReentrant { // <= FOUND
375:         uint256 amount = deposits[depositor].amount;
376:         if (amount == 0) {
377:             revert LiquidityPoolRouter__NoOngoingDeposit();
378:         }
379: 
380:         uint256 initializedAt = deposits[depositor].initializedAt;
381:         _validateTimelockIsOver(initializedAt);
382:         _validateFinalizationIsOpenForAll(depositor, initializedAt);
383: 
384:         address payable liquidityPool = payable(deposits[depositor].liquidityPool);
385:         address token = ERC4626(liquidityPool).asset();
386:         uint256 expectedShares = deposits[depositor].expectedShares;
387:         uint256 actualShares = ERC4626(liquidityPool).previewDeposit(amount);
388:         uint256 incentive = deposits[depositor].finalizationIncentive;
389: 
390:         deposits[depositor] = Deposit(address(0), 0, 0, 0, 0); // <= FOUND
391:         pendingDeposits[liquidityPool] -= amount;
392: 
393:         _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, incentive, 2_300);
394: 
395:         uint256 sharesMinted;
396:         uint256 amountRequired;
397:         if (expectedShares >= actualShares) {
398:             amountRequired = amount;
399:             sharesMinted = _deposit(token, liquidityPool, amountRequired, depositor);
400:         } else {
401:             amountRequired = ERC4626(liquidityPool).previewMint(expectedShares);
402:             sharesMinted = _deposit(token, liquidityPool, amountRequired, depositor);
403:             if (token == WETH) {
404:                 _transferETHAndWrapIfFailWithGasLimit(WETH, liquidityPool, amount - amountRequired, gasleft());
405:             } else {
406:                 _executeERC20DirectTransfer(token, liquidityPool, amount - amountRequired);
407:             }
408:         }
409: 
410:         emit LiquidityPoolRouter__DepositFinalized(msg.sender, depositor, liquidityPool, amountRequired, sharesMinted);
411:     }

['458']

458:     function finalizeRedemption(address redeemer) external nonReentrant { // <= FOUND
459:         uint256 amount = redemptions[redeemer].shares;
460:         if (amount == 0) {
461:             revert LiquidityPoolRouter__NoOngoingRedemption();
462:         }
463: 
464:         uint256 initializedAt = redemptions[redeemer].initializedAt;
465:         _validateTimelockIsOver(initializedAt);
466:         _validateFinalizationIsOpenForAll(redeemer, initializedAt);
467: 
468:         address payable liquidityPool = payable(redemptions[redeemer].liquidityPool);
469:         address token = ERC4626(liquidityPool).asset();
470:         uint256 expectedAssets = redemptions[redeemer].expectedAssets;
471:         uint256 incentive = redemptions[redeemer].finalizationIncentive;
472: 
473:         redemptions[redeemer] = Redemption(address(0), 0, 0, 0, 0); // <= FOUND
474: 
475:         uint256 assetsRedeemed = LiquidityPool(liquidityPool).redeem(amount, address(this), address(this));
476: 
477:         _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, incentive, 2_300);
478: 
479:         if (expectedAssets >= assetsRedeemed) {
480:             _transferAssetsRedeemed(token, redeemer, assetsRedeemed);
481:         } else {
482:             _transferAssetsRedeemed(token, redeemer, expectedAssets);
483:             _executeERC20DirectTransfer(token, liquidityPool, assetsRedeemed - expectedAssets);
484:             assetsRedeemed = expectedAssets;
485:         }
486: 
487:         emit LiquidityPoolRouter__RedemptionFinalized(msg.sender, redeemer, liquidityPool, amount, assetsRedeemed);
488:     }

[Low-12] Contract can be bricked by the use of both 'Ownable' and 'Pausable' in the same contract

Resolution

In Solidity, the Ownable and Pausable contract inheritances add control mechanisms that can affect functionality. If a contract using Pausable is paused and ownership is then renounced through Ownable, the ability to resume operations is permanently lost, as only the owner could call the unpause function. To avoid this, developers should either disable the renounceOwnership function entirely or implement mechanisms to ensure unpause capability before renouncing ownership. This will help to prevent irreversible contract freezing and loss of use of functions using the whenNotPaused modifier and ensure sustained control over the contract's state.

Num of instances: 1

Findings

Click to show findings

['26']

26: contract LiquidityPoolRouter is OwnableTwoSteps, LowLevelWETH, LowLevelERC20Transfer, ReentrancyGuard, Pausable 

[Low-13] Constant decimal values

Resolution

The use of fixed decimal values such as 1e18 or 1e8 in Solidity contracts can lead to inaccuracies, bugs, and vulnerabilities, particularly when interacting with tokens having different decimal configurations. Not all ERC20 tokens follow the standard 18 decimal places, and assumptions about decimal places can lead to miscalculations.

Resolution: Always retrieve and use the decimals() function from the token contract itself when performing calculations involving token amounts. This ensures that your contract correctly handles tokens with any number of decimal places, mitigating the risk of numerical errors or under/overflows that could jeopardize contract integrity and user funds.

Num of instances: 9

Findings

Click to show findings

['154']

154:                 winProbabilities[lavasCount][revealedTilesCount] = (numerator * 1e18) / denominator; // <= FOUND

['433']

433:         return
434:             ((1e18 - winProbability) * KELLY_FRACTION_SCALER) / // <= FOUND
435:             ((multiplier * _liquidityProviderAdjustedReturn()) / 10_000 - 1e4) - // <= FOUND
436:             (winProbability * KELLY_FRACTION_SCALER) /
437:             1e4;

['586']

586:         multiplier = (10_000 * 1e18) / winProbability; // <= FOUND

['307']

307:         Fee memory fee = Fee({
308:             protocolFee: (game.params.playAmountPerRound * multiplier * feeSplit.protocolFeeBasisPoints) / 1e8, // <= FOUND
309:             liquidityPoolFee: (game.params.playAmountPerRound * multiplier * feeSplit.liquidityPoolFeeBasisPoints) / 1e8 // <= FOUND
310:         });

['341']

341:                         uint256 protocolFee = (playAmountPerRound * multiplier * feeSplit.protocolFeeBasisPoints) / 1e8; // <= FOUND

['500']

500:                             Fee memory fee = Fee({
501:                                 protocolFee: (game.params.playAmountPerRound *
502:                                     multiplier *
503:                                     feeSplit.protocolFeeBasisPoints) / 1e8, // <= FOUND
504:                                 liquidityPoolFee: (game.params.playAmountPerRound *
505:                                     multiplier *
506:                                     feeSplit.liquidityPoolFeeBasisPoints) / 1e8 // <= FOUND
507:                             });

['342']

342:                         uint256 liquidityPoolFee = (playAmountPerRound *
343:                             multiplier *
344:                             adjustedLiquidityPoolFeeBasisPoints) / 1e8; // <= FOUND

['280']

280:                             uint256 protocolFee = (game.multiplier *
281:                                 game.params.playAmountPerRound *
282:                                 feeSplit.protocolFeeBasisPoints) / 1e8; // <= FOUND

['283']

283:                             uint256 liquidityPoolFee = (game.multiplier *
284:                                 game.params.playAmountPerRound *
285:                                 feeSplit.liquidityPoolFeeBasisPoints) / 1e8; // <= FOUND

[Low-14] No access control on receive/payable fallback

Resolution

Without access control on receive/payable fallback functions in a contract, anyone can send Ether (ETH) to the contract's address. If there's no way to withdraw those funds defined within the contract, any Ether sent, whether intentionally or by mistake, will be permanently stuck. This could lead to unintended loss of funds. Implementing proper access control ensures that only authorized addresses can interact with these functions. Resolution could involve adding access control mechanisms, like Ownable or specific permission requirements, or creating a withdrawal function accessible only to the contract's owner, thus preventing unintentional loss of funds.

Num of instances: 1

Findings

Click to show findings

['506']

506:     receive() external payable {} // <= FOUND

[Low-15] Critical functions should have a timelock

Resolution

Critical functions, especially those affecting protocol parameters or user funds, are potential points of failure or exploitation. To mitigate risks, incorporating a timelock on such functions can be beneficial. A timelock requires a waiting period between the time an action is initiated and when it's executed, giving stakeholders time to react, potentially vetoing malicious or erroneous changes. To implement, integrate a smart contract like OpenZeppelin's TimelockController or build a custom mechanism. This ensures governance decisions or administrative changes are transparent and allows for community or multi-signature interventions, enhancing protocol security and trustworthiness.

Num of instances: 9

Findings

Click to show findings

['225']

225:     function setElapsedTimeRequiredForRefund(uint40 _elapsedTimeRequiredForRefund) external onlyOwner  // <= FOUND

['242']

242:     function setMaximumNumberOfRounds(uint16 _maximumNumberOfRounds) external onlyOwner  // <= FOUND

['250']

250:     function setGameKellyFractionBasisPoints(address game, uint256 basisPoints) external onlyOwner  // <= FOUND

['262']

262:     function setVrfParameters(VrfParameters memory _vrfParameters) external onlyOwner  // <= FOUND

['270']

270:     function setVrfFeeRecipient(address _vrfFeeRecipient) external onlyOwner  // <= FOUND

['279']

279:     function setProtocolFeeRecipient(address _protocolFeeRecipient) external onlyOwner  // <= FOUND

['293']

293:     function setFeeSplit( // <= FOUND
294:         address _game,
295:         uint16 _protocolFeeBasisPoints,
296:         uint16 _liquidityPoolFeeBasisPoints
297:     ) external onlyOwner 

['117']

117:     function setMultipliers(uint128 riskLevel, uint128 rowCount, uint256[] memory _multipliers) external onlyOwner  // <= FOUND

['245']

245:     function setDepositLimit( // <= FOUND
246:         address liquidityPool,
247:         uint256 minDepositAmount,
248:         uint256 maxDepositAmount,
249:         uint256 maxBalance
250:     ) external onlyOwner 

[Low-16] Consider implementing two-step procedure for updating protocol addresses

Resolution

Implementing a two-step procedure for updating protocol addresses adds an extra layer of security. In such a system, the first step initiates the change, and the second step, after a predefined delay, confirms and finalizes it. This delay allows stakeholders or monitoring tools to observe and react to unintended or malicious changes. If an unauthorized change is detected, corrective actions can be taken before the change is finalized. To achieve this, introduce a "proposed address" state variable and a "delay period". Upon an update request, set the "proposed address". After the delay, if not contested, the main protocol address can be updated.

Num of instances: 2

Findings

Click to show findings

['270']

270:     function setVrfFeeRecipient(address _vrfFeeRecipient) external onlyOwner { // <= FOUND
271:         vrfFeeRecipient = _vrfFeeRecipient;
272:         emit VrfFeeRecipientUpdated(_vrfFeeRecipient);
273:     }

['279']

279:     function setProtocolFeeRecipient(address _protocolFeeRecipient) external onlyOwner { // <= FOUND
280:         protocolFeeRecipient = _protocolFeeRecipient;
281:         emit ProtocolFeeRecipientUpdated(_protocolFeeRecipient);
282:     }

[Low-17] Don't assume specific ETH balance

Resolution

ETH balances can change due to gas costs, miner reordering, or unexpected transactions. Directly comparing an expected balance with == or != can lead to unpredictable results. Instead, use range checks or require a minimum balance. For instance, instead of requiring an exact balance, check if an account's balance is within an acceptable range or above a certain threshold. This provides flexibility and accounts for slight variations that might occur due to the dynamic nature of blockchain transactions and fees. By avoiding exact balance checks, smart contracts can avoid unintentional failures and enhance robustness.

Num of instances: 5

Findings

Click to show findings

['272']

272:         if (msg.value != vrfFee) { // <= FOUND

['203']

203:             if (msg.value != playAmountPerRound * numberOfRounds + vrfFee) { // <= FOUND

['207']

207:             if (msg.value != vrfFee) { // <= FOUND

['661']

661:         if (msg.value != finalizationParams.finalizationIncentive) { // <= FOUND

['287']

287:         if (amount + finalizationParams.finalizationIncentive != msg.value) { // <= FOUND

[Low-18] State variables not capped at reasonable values

Resolution

Setting boundaries on state variables in smart contracts is essential for maintaining system integrity and user protection. Without caps on values, variables could reach extremes that exploit or disrupt contract functionality, leading to potential loss or unintended consequences for users. Implementing checks for minimum and maximum permissible values can prevent such issues, ensuring variables remain within a safe and reasonable range. This practice guards against attacks aimed at destabilizing the contract, such as griefing, where attackers intentionally cause distress by exploiting vulnerabilities. Proper validation promotes contract reliability, user trust, and a healthier ecosystem by mitigating risks associated with unbounded state changes.

Num of instances: 1

Findings

Click to show findings

['117']

117:     function setMultipliers(uint128 riskLevel, uint128 rowCount, uint256[] memory _multipliers) external onlyOwner {
118:         _validateRiskLevel(riskLevel);
119:         _validateRowCount(rowCount);
120: 
121:         uint256 binsCount = _multipliers.length; // <= FOUND
122: 
123:         if (binsCount != rowCount + 1) {
124:             revert LaserBlast__BinsCountMustBeOneHigherThanRowCount();
125:         }
126: 
127:         for (uint256 i; i < binsCount; ++i) {
128:             uint256 multiplier = _multipliers[i]; // <= FOUND
129:             if (multiplier == 0) {
130:                 revert Game__ZeroMultiplier();
131:             }
132: 
133:             bytes32 key = _multiplierKey(riskLevel, rowCount, i); // <= FOUND
134: 
135:             if (multipliers[key] != 0) { // <= FOUND
136:                 revert LaserBlast__MultiplierAlreadySet();
137:             }
138: 
139:             multipliers[key] = multiplier; // <= FOUND
140:         }
141: 
142:         emit LaserBlast__MultipliersSet(riskLevel, rowCount, _multipliers);
143:     }

[Low-19] Consider a uptime feed on L2 deployments to prevent issues caused by downtime

Resolution

In L2 deployments, incorporating an uptime feed is crucial to mitigate issues arising from sequencer downtime. Downtime can disrupt services, leading to transaction failures or incorrect data readings, affecting overall system reliability. By integrating an uptime feed, you gain insight into the operational status of the L2 network, enabling proactive measures like halting sensitive operations or alerting users. This approach ensures that your contract behaves predictably and securely during network outages, enhancing the robustness and reliability of your decentralized application, which is especially important in mission-critical or high-stakes environments.

Num of instances: 10

Findings

Click to show findings

['13']

13: contract DontFallIn is Game  // <= FOUND

['16']

16: contract ERC20LiquidityPool is LiquidityPool  // <= FOUND

['18']

18: contract EthLiquidityPool is LiquidityPool, LowLevelWETH  // <= FOUND

['13']

13: contract Flipper is Game  // <= FOUND

['17']

17: contract GameConfigurationManager is OwnableTwoSteps, IGameConfigurationManager  // <= FOUND

['13']

13: contract LaserBlast is Game  // <= FOUND

['26']

26: contract LiquidityPoolRouter is OwnableTwoSteps, LowLevelWETH, LowLevelERC20Transfer, ReentrancyGuard, Pausable  // <= FOUND

['13']

13: contract Quantum is Game  // <= FOUND

['19']

19: abstract contract Game is ReentrancyGuard, LowLevelWETH, LowLevelERC20Transfer, VRFConsumerBaseV2, OwnableTwoSteps  // <= FOUND

['24']

24: abstract contract LiquidityPool is // <= FOUND
25:     ERC4626,
26:     OwnableTwoSteps,
27:     ReentrancyGuard,
28:     Pausable,
29:     ILiquidityPool,
30:     LowLevelERC20Transfer
31: 

[Low-20] Non constant/immutable state variables are missing a setter post deployment

Resolution

Non-constant or non-immutable state variables lacking a setter function can create inflexibility in contract operations. If there's no way to update these variables post-deployment, the contract might not adapt to changing conditions or requirements, which can be a significant drawback, especially in upgradable or long-lived contracts. To resolve this, implement setter functions guarded by appropriate access controls, like onlyOwner or similar modifiers, so that these variables can be updated as required while maintaining security. This enables smoother contract maintenance and feature upgrades.

Num of instances: 2

Findings

Click to show findings

['73']

73:     
83:     uint256[GRID_SIZE] public maximumRevealableTiles;

['56']

56:     
63:     uint256[9][3] private kellyFractions;

[NonCritical-1] Important function with no access control

Num of instances: 1

Findings

Click to show findings

['97']

97:     function withdraw(uint256, address, address) public pure override returns (uint256)  // <= FOUND

[NonCritical-2] Unnecessary struct attribute prefix

Resolution

In struct definitions, using redundant prefixes for attributes is unnecessary. For instance, in a struct named Employee, attributes like employeeName, employeeID, and employeeEmail can be simplified to name, ID, and email respectively, since they are already inherently associated with Employee. By removing these repetitive prefixes, the code becomes more concise and easier to read, maintaining its contextual clarity.

Num of instances: 2

Findings

Click to show findings

['48']

48:     struct VrfParameters { // <= FOUND
49:         address coordinator;
50:         uint64 subscriptionId;
51:         uint32 callbackGasLimit;
52:         uint16 minimumRequestConfirmations;
53:         uint240 vrfFee; // <= FOUND
54:         bytes32 keyHash;
55:     }

['138']

138:     struct FinalizationParams { // <= FOUND
139:         uint80 timelockDelay;
140:         uint80 finalizationForAllDelay; // <= FOUND
141:         uint80 finalizationIncentive; // <= FOUND
142:     }

[NonCritical-3] Using abi.encodePacked can result in hash collision when used in hashing functions

Resolution

Consider using abi.encode as this pads data to 32 byte segments

Num of instances: 2

Findings

Click to show findings

['180']

180:             bytes32 requestId = keccak256(abi.encodePacked(game, currency, liquidityPool));

['180']

180:         bytes32 requestId = keccak256(abi.encodePacked(game, currency, liquidityPool));

[NonCritical-4] Default address(0) can be returned

Resolution

Allowing a function in Solidity to return the default address (address(0)) can be problematic as it can represent uninitialized or invalid addresses. If such an address is utilized in transfer operations or other sensitive actions, it could lead to loss of funds or unpredicted behavior. It's prudent to include checks in your functions to prevent the return of the zero address, enhancing contract security.

Num of instances: 1

Findings

Click to show findings

['337']

337:     function getGameLiquidityPool(address game, address currency) external view returns (address liquidityPool) {
338:         liquidityPool = gameLiquidityPool[game][currency];
339:     }

[NonCritical-5] Owner can renounce while system is paused

Resolution

If an owner renounces their role while a system is paused, it could lead to permanent inaccessibility of assets stored in the contract. Such a scenario jeopardizes the trust and functionality of the protocol, as no one would be able to unpause the system or access funds. To mitigate this risk, it's essential to implement a mechanism that either prevents the owner from renouncing while the system is paused or requires multiple signatories to perform such critical actions. By introducing such safeguards, it ensures that user assets are not rendered inaccessible due to a single user's action or oversight.

Num of instances: 2

Findings

Click to show findings

['24']

24: abstract contract LiquidityPool is
25:     ERC4626,
26:     OwnableTwoSteps, // <= FOUND
27:     ReentrancyGuard,
28:     Pausable, // <= FOUND
29:     ILiquidityPool,
30:     LowLevelERC20Transfer
31: 

['26']

26: contract LiquidityPoolRouter is OwnableTwoSteps, LowLevelWETH, LowLevelERC20Transfer, ReentrancyGuard, Pausable  // <= FOUND

[NonCritical-6] Events regarding state variable changes should emit the previous state variable value

Resolution

Modify such events to contain the previous value of the state variable as demonstrated in the example below

Num of instances: 10

Findings

Click to show findings

['79']

79: event LaserBlast__MultipliersSet(uint256 riskLevel, uint256 rowCount, uint256[] multipliers);

['88']

88: event ElapsedTimeRequiredForRefundUpdated(uint256 _elapsedTimeRequiredForRefund);

['89']

89: event FeeSplitUpdated(address game, uint256 protocolFeeBasisPoints, uint256 liquidityPoolFeeBasisPoints);

['90']

90: event VrfFeeRecipientUpdated(address _vrfFeeRecipient);

['94']

94: event GameKellyFractionBasisPointsUpdated(address game, uint256 basisPoints);

['95']

95: event MaximumNumberOfRoundsUpdated(uint256 _maximumNumberOfRounds);

['96']

96: event ProtocolFeeRecipientUpdated(address _protocolFeeRecipient);

['97']

97: event VrfParametersUpdated(
98:         address coordinator,
99:         uint64 subscriptionId,
100:         uint32 callbackGasLimit,
101:         uint16 minimumRequestConfirmations,
102:         uint240 vrfFee,
103:         bytes32 keyHash
104:     );

['43']

43: event LiquidityPoolRouter__DepositLimitUpdated(
44:         address liquidityPool,
45:         uint256 minDepositAmount,
46:         uint256 maxDepositAmount,
47:         uint256 maxBalance
48:     );

['49']

49: event LiquidityPoolRouter__FinalizationParamsUpdated(
50:         uint256 timelockDelay,
51:         uint256 finalizationForAllDelay,
52:         uint256 finalizationIncentive
53:     );

[NonCritical-7] In functions which accept an address as a parameter, there should be a zero address check to prevent bugs

Resolution

In smart contract development, especially with Solidity, it's crucial to validate inputs to functions. When a function accepts an Ethereum address as a parameter, implementing a zero address check (i.e., ensuring the address is not 0x0) is a best practice to prevent potential bugs and vulnerabilities. The zero address (0x0) is a default value and generally indicates an uninitialized or invalid state. Passing the zero address to certain functions can lead to unintended behaviors, like funds getting locked permanently or transactions failing silently. By checking for and rejecting the zero address, developers can ensure that the function operates as intended and interacts only with valid Ethereum addresses. This check enhances the contract's robustness and security.

Num of instances: 47

Findings

Click to show findings

['168']

168:     function play(
169:         uint256 playAmountPerRound,
170:         address currency,
171:         uint8 lavasCount,
172:         uint32 selectedTiles,
173:         bool cashoutIfWon
174:     ) external payable nonReentrant 

['381']

381:     function maxPlayAmountPerGame(
382:         address currency,
383:         uint256 lavasCount,
384:         uint256 selectedTilesCount
385:     ) public view returns (uint256 maxPlayAmount) 

['409']

409:     function minPlayAmountPerGame(
410:         address currency,
411:         uint256 lavasCount,
412:         uint256 selectedTilesCount
413:     ) public view returns (uint256 minPlayAmount) 

['619']

619:     function _deleteGame(address player) private 

['61']

61:     function transferPayoutToPlayer(
62:         address game,
63:         uint256 amount,
64:         address receiver
65:     ) external nonReentrant whenNotPaused 

['80']

80:     function transferProtocolFee(
81:         address game,
82:         uint256 amount,
83:         address protocolFeeRecipient
84:     ) external nonReentrant whenNotPaused 

['61']

61:     function transferPayoutToPlayer(
62:         address game,
63:         uint256 amount,
64:         address receiver
65:     ) external nonReentrant whenNotPaused 

['80']

80:     function transferProtocolFee(
81:         address game,
82:         uint256 amount,
83:         address protocolFeeRecipient
84:     ) external nonReentrant whenNotPaused 

['75']

75:     function play(
76:         uint16 numberOfRounds,
77:         uint256 playAmountPerRound,
78:         address currency,
79:         int256 stopGain,
80:         int256 stopLoss,
81:         bool isGold
82:     ) external payable nonReentrant 

['167']

167:     function maxPlayAmountPerGame(address currency) public view returns (uint256 maxPlayAmount) 

['181']

181:     function minPlayAmountPerGame(address currency) public view returns (uint256 minPlayAmount) 

['619']

619:     function _deleteGame(address player) private 

['112']

112:     function claimYield(address receiver) external onlyOwner 

['249']

249:     function _handlePayout(
250:         address player,
251:         Game__GameParams storage params,
252:         uint256 numberOfRoundsPlayed,
253:         uint256 payout,
254:         uint256 protocolFee
255:     ) internal 

['189']

189:     function confirmGameLiquidityPoolConnectionRequest(
190:         address game,
191:         address currency,
192:         address liquidityPool
193:     ) external onlyOwner 

['214']

214:     function disconnectGameFromLiquidityPool(address game, address currency) external onlyOwner 

['250']

250:     function setGameKellyFractionBasisPoints(address game, uint256 basisPoints) external onlyOwner 

['270']

270:     function setVrfFeeRecipient(address _vrfFeeRecipient) external onlyOwner 

['279']

279:     function setProtocolFeeRecipient(address _protocolFeeRecipient) external onlyOwner 

['293']

293:     function setFeeSplit(
294:         address _game,
295:         uint16 _protocolFeeBasisPoints,
296:         uint16 _liquidityPoolFeeBasisPoints
297:     ) external onlyOwner 

['337']

337:     function getGameLiquidityPool(address game, address currency) external view returns (address liquidityPool) 

['344']

344:     function getFeeSplit(address game) external view override returns (FeeSplit memory) 

['155']

155:     function play(
156:         uint16 numberOfRounds,
157:         uint256 playAmountPerRound,
158:         address currency,
159:         int256 stopGain,
160:         int256 stopLoss,
161:         uint128 riskLevel,
162:         uint128 rowCount
163:     ) external payable nonReentrant 

['254']

254:     function maxPlayAmountPerGame(
255:         address currency,
256:         uint128 riskLevel,
257:         uint128 rowCount
258:     ) public view returns (uint256 maxPlayAmount) 

['282']

282:     function minPlayAmountPerGame(
283:         address currency,
284:         uint128 riskLevel,
285:         uint128 rowCount
286:     ) public view returns (uint256 minPlayAmount) 

['619']

619:     function _deleteGame(address player) private 

['74']

74:     function deposit(uint256 assets, address receiver) public override returns (uint256) 

['82']

82:     function redeem(uint256 shares, address receiver, address owner) public override returns (uint256) 

['90']

90:     function mint(uint256, address) public pure override returns (uint256) 

['97']

97:     function withdraw(uint256, address, address) public pure override returns (uint256) 

['112']

112:     function claimYield(address receiver) external onlyOwner 

['140']

140:     function _withdraw(
141:         address caller,
142:         address receiver,
143:         address owner,
144:         uint256 assets,
145:         uint256 shares
146:     ) internal override 

['245']

245:     function setDepositLimit(
246:         address liquidityPool,
247:         uint256 minDepositAmount,
248:         uint256 maxDepositAmount,
249:         uint256 maxBalance
250:     ) external onlyOwner 

['329']

329:     function deposit(address token, uint256 amount) external payable nonReentrant whenNotPaused 

['374']

374:     function finalizeDeposit(address depositor) external nonReentrant 

['420']

420:     function redeem(address token, uint256 amount) external payable nonReentrant 

['458']

458:     function finalizeRedemption(address redeemer) external nonReentrant 

['112']

112:     function claimYield(address receiver) external onlyOwner 

['516']

516:     function _deposit(
517:         address token,
518:         address liquidityPool,
519:         uint256 amount,
520:         address depositor
521:     ) private returns (uint256 sharesMinted) 

['536']

536:     function _transferAssetsRedeemed(address token, address redeemer, uint256 assetsRedeemed) private 

['551']

551:     function _claimYield(address token, address receiver) private 

['612']

612:     function _validateDepositAmount(address liquidityPool, uint256 amount) private view 

['639']

639:     function _validateFinalizationIsOpenForAll(address requester, uint256 initializedAt) private view 

['95']

95:     function play(
96:         uint16 numberOfRounds,
97:         uint256 playAmountPerRound,
98:         address currency,
99:         int256 stopGain,
100:         int256 stopLoss,
101:         bool isAbove,
102:         uint248 multiplier
103:     ) external payable nonReentrant 

['176']

176:     function maxPlayAmountPerGame(address currency, uint256 multiplier) public view returns (uint256 maxPlayAmount) 

['193']

193:     function minPlayAmountPerGame(address currency, uint256 multiplier) public view returns (uint256 minPlayAmount) 

['619']

619:     function _deleteGame(address player) private 

[NonCritical-8] Enum values should be used in place of constant array indexes

Resolution

Create a commented enum value to use in place of constant array indexes, this makes the code far easier to understand

Num of instances: 4

Findings

Click to show findings

['468']

468:                     runningGameState.randomWord = randomWords[0]; // <= FOUND

['104']

104:         kellyFractions[0] = [41300, 25900, 31400, 29500, 63700, 49000, 49700, 47300, 28800]; // <= FOUND

['105']

105:         kellyFractions[1] = [6320, 9480, 11300, 9920, 10920, 10280, 9460, 8490, 7350]; // <= FOUND

['106']

106:         kellyFractions[2] = [4049, 2689, 1563, 1599, 1247, 869, 547, 634, 369]; // <= FOUND

[NonCritical-9] Revert statements within external and public functions can be used to perform DOS attacks

Resolution

In Solidity, 'revert' statements are used to undo changes and throw an exception when certain conditions are not met. However, in public and external functions, improper use of revert can be exploited for Denial of Service (DoS) attacks. An attacker can intentionally trigger these 'revert' conditions, causing legitimate transactions to consistently fail. For example, if a function relies on specific conditions from user input or contract state, an attacker could manipulate these to continually force reverts, blocking the function's execution. Therefore, it's crucial to design contract logic to handle exceptions properly and avoid scenarios where revert can be predictably triggered by malicious actors. This includes careful input validation and considering alternative design patterns that are less susceptible to such abuses.

Num of instances: 27

Findings

Click to show findings

['168']

168:     function play(
169:         uint256 playAmountPerRound,
170:         address currency,
171:         uint8 lavasCount,
172:         uint32 selectedTiles,
173:         bool cashoutIfWon
174:     ) external payable nonReentrant {
175:         uint16 numberOfRounds = 1;
176: 
177:         _validateNumberOfRoundsAndPlayAmountPerRound(numberOfRounds, playAmountPerRound);
178:         _validateNoOngoingRound(games[msg.sender].params.numberOfRounds);
179: 
180:         _validateSelectedTilesMaxSize(selectedTiles);
181:         (uint256 selectedTilesCount, ) = _nonZeroTilesCount(selectedTiles);
182:         _validateNonZeroSelectedTiles(selectedTilesCount);
183:         _validateNonZeroMultiplier(lavasCount, selectedTilesCount);
184: 
185:         uint256 _maxPlayAmountPerGame = maxPlayAmountPerGame(
186:             currency,
187:             lavasCount,
188:             cashoutIfWon ? selectedTilesCount : maximumRevealableTiles[lavasCount]
189:         );
190: 
191:         if (playAmountPerRound > _maxPlayAmountPerGame) {
192:             revert Game__PlayAmountPerRoundTooHigh(); // <= FOUND
193:         }
194: 
195:         if (playAmountPerRound < _minPlayAmountPerGame(_maxPlayAmountPerGame)) {
196:             revert Game__PlayAmountPerRoundTooLow(); // <= FOUND
197:         }
198: 
199:         
200:         
201:         
202:         _getGameLiquidityPool(currency);
203: 
204:         uint256 vrfFee = _requestRandomness();
205: 
206:         _escrowPlayAmountAndChargeVrfFee(currency, 1, playAmountPerRound, vrfFee);
207: 
208:         games[msg.sender].params = Game__GameParams({
209:             blockNumber: uint40(block.number),
210:             numberOfRounds: numberOfRounds,
211:             playAmountPerRound: playAmountPerRound,
212:             currency: currency,
213:             stopGain: 0,
214:             stopLoss: 0,
215:             randomnessRequestedAt: uint40(block.timestamp),
216:             vrfFee: vrfFee
217:         });
218:         games[msg.sender].lavasCount = lavasCount;
219:         games[msg.sender].selectedTiles = selectedTiles;
220:         games[msg.sender].cashoutIfWon = cashoutIfWon;
221: 
222:         emit DontFallIn__GameCreated(
223:             block.number,
224:             msg.sender,
225:             playAmountPerRound,
226:             currency,
227:             lavasCount,
228:             selectedTiles,
229:             cashoutIfWon
230:         );
231:     }

['242']

242:     function playOngoing(uint32 selectedTiles, bool cashoutIfWon) external payable nonReentrant {
243:         DontFallIn__Game storage game = games[msg.sender];
244: 
245:         if (game.params.randomnessRequestedAt != 0) {
246:             revert Game__OngoingRound(); // <= FOUND
247:         }
248: 
249:         if (game.params.numberOfRounds == 0) {
250:             revert Game__NoOngoingRound(); // <= FOUND
251:         }
252: 
253:         uint256 grid = game.grid;
254: 
255:         if (grid & selectedTiles != 0) {
256:             revert DontFallIn__TilesAlreadyRevealed(); // <= FOUND
257:         }
258: 
259:         _validateSelectedTilesMaxSize(selectedTiles);
260:         (uint256 selectedTilesCount, ) = _nonZeroTilesCount(selectedTiles);
261:         _validateNonZeroSelectedTiles(selectedTilesCount);
262: 
263:         (uint256 revealedTilesCount, ) = _nonZeroTilesCount(grid);
264:         _validateNonZeroMultiplier(game.lavasCount, selectedTilesCount + revealedTilesCount);
265: 
266:         
267:         
268:         
269:         _getGameLiquidityPool(game.params.currency);
270: 
271:         (, , , , uint240 vrfFee, ) = GAME_CONFIGURATION_MANAGER.vrfParameters();
272:         if (msg.value != vrfFee) {
273:             revert Game__InexactNativeTokensSupplied(); // <= FOUND
274:         }
275: 
276:         games[msg.sender].selectedTiles = selectedTiles;
277:         games[msg.sender].cashoutIfWon = cashoutIfWon;
278:         games[msg.sender].params.randomnessRequestedAt = uint40(block.timestamp);
279:         games[msg.sender].params.vrfFee = vrfFee;
280: 
281:         _requestRandomness();
282: 
283:         emit DontFallIn__GameContinued(game.params.blockNumber, msg.sender, selectedTiles, cashoutIfWon);
284:     }

['289']

289:     function cashout() external nonReentrant {
290:         DontFallIn__Game storage game = games[msg.sender];
291: 
292:         if (game.params.randomnessRequestedAt != 0) {
293:             revert Game__OngoingRound(); // <= FOUND
294:         }
295: 
296:         uint256 multiplier = game.multiplier;
297:         if (multiplier == 0) {
298:             revert Game__ZeroMultiplier(); // <= FOUND
299:         }
300: 
301:         
302:         
303:         
304:         _getGameLiquidityPool(game.params.currency);
305: 
306:         IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(address(this));
307:         Fee memory fee = Fee({
308:             protocolFee: (game.params.playAmountPerRound * multiplier * feeSplit.protocolFeeBasisPoints) / 1e8,
309:             liquidityPoolFee: (game.params.playAmountPerRound * multiplier * feeSplit.liquidityPoolFeeBasisPoints) / 1e8
310:         });
311: 
312:         uint256 payout = ((game.params.playAmountPerRound * multiplier) / 10_000) -
313:             fee.protocolFee -
314:             fee.liquidityPoolFee;
315:         _handlePayout(msg.sender, game.params, 1, payout, fee.protocolFee);
316: 
317:         emit DontFallIn__GameCashedOut(
318:             game.params.blockNumber,
319:             msg.sender,
320:             payout,
321:             multiplier,
322:             fee.protocolFee,
323:             fee.liquidityPoolFee
324:         );
325: 
326:         _deleteGame(msg.sender);
327:     }

['333']

333:     function cashoutOriginalAmount() external nonReentrant {
334:         DontFallIn__Game storage game = games[msg.sender];
335: 
336:         if (game.params.randomnessRequestedAt != 0) {
337:             revert Game__OngoingRound(); // <= FOUND
338:         }
339: 
340:         uint256 playAmountPerRound = game.params.playAmountPerRound;
341:         if (playAmountPerRound == 0) {
342:             revert Game__ZeroPlayAmountPerRound(); // <= FOUND
343:         }
344: 
345:         address currency = game.params.currency;
346: 
347:         if (GAME_CONFIGURATION_MANAGER.getGameLiquidityPool(address(this), currency) != address(0)) {
348:             revert Game__LiquidityPoolConnected(); // <= FOUND
349:         }
350: 
351:         emit DontFallIn__GameCashedOutOriginalAmount(game.params.blockNumber, msg.sender, playAmountPerRound);
352: 
353:         _deleteGame(msg.sender);
354: 
355:         if (currency == address(0)) {
356:             _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, playAmountPerRound, gasleft());
357:         } else {
358:             _executeERC20DirectTransfer(currency, msg.sender, playAmountPerRound);
359:         }
360:     }

['447']

447:     function getMultiplier(uint256 lavasCount, uint256 revealedTilesCount) external view returns (uint256 multiplier) {
448:         uint256 winProbability = winProbabilities[lavasCount][revealedTilesCount];
449:         if (winProbability == 0) {
450:             revert Game__ZeroMultiplier(); // <= FOUND
451:         }
452:         multiplier = _getMultiplier(winProbability);
453:     }

['75']

75:     function play(
76:         uint16 numberOfRounds,
77:         uint256 playAmountPerRound,
78:         address currency,
79:         int256 stopGain,
80:         int256 stopLoss,
81:         bool isGold
82:     ) external payable nonReentrant {
83:         _validateNumberOfRoundsAndPlayAmountPerRound(numberOfRounds, playAmountPerRound);
84: 
85:         uint256 _maxPlayAmountPerGame = maxPlayAmountPerGame(currency);
86:         uint256 totalPlayAmount = playAmountPerRound * numberOfRounds;
87: 
88:         if (totalPlayAmount > _maxPlayAmountPerGame) {
89:             revert Game__PlayAmountPerRoundTooHigh(); // <= FOUND
90:         }
91: 
92:         if (totalPlayAmount < _minPlayAmountPerGame(_maxPlayAmountPerGame)) {
93:             revert Game__PlayAmountPerRoundTooLow(); // <= FOUND
94:         }
95: 
96:         _validateNoOngoingRound(games[msg.sender].params.numberOfRounds);
97:         _validateStopGainAndLoss(stopGain, stopLoss);
98: 
99:         
100:         
101:         
102:         _getGameLiquidityPool(currency);
103: 
104:         uint256 vrfFee = _requestRandomness();
105: 
106:         _escrowPlayAmountAndChargeVrfFee(currency, numberOfRounds, playAmountPerRound, vrfFee);
107: 
108:         games[msg.sender] = Flipper__Game({
109:             params: Game__GameParams({
110:                 blockNumber: uint40(block.number),
111:                 numberOfRounds: numberOfRounds,
112:                 playAmountPerRound: playAmountPerRound,
113:                 currency: currency,
114:                 stopGain: stopGain,
115:                 stopLoss: stopLoss,
116:                 randomnessRequestedAt: uint40(block.timestamp),
117:                 vrfFee: vrfFee
118:             }),
119:             isGold: isGold
120:         });
121: 
122:         emit Flipper__GameCreated(
123:             block.number,
124:             msg.sender,
125:             numberOfRounds,
126:             playAmountPerRound,
127:             currency,
128:             stopGain,
129:             stopLoss,
130:             isGold
131:         );
132:     }

['164']

164:     function initiateGameLiquidityPoolConnectionRequest(
165:         address game,
166:         address currency,
167:         address liquidityPool
168:     ) external onlyOwner {
169:         address liquidityPoolCurrency = IERC4626(liquidityPool).asset();
170: 
171:         if ((currency != liquidityPoolCurrency) && !(currency == address(0) && liquidityPoolCurrency == WETH)) {
172:             revert GameConfigurationManager__GameLiquidityPoolCurrencyMismatch(); // <= FOUND
173:         }
174: 
175:         if (IERC4626(liquidityPool).totalSupply() == 0) {
176:             gameLiquidityPool[game][currency] = liquidityPool;
177:             kellyFractionBasisPoints[game] = 10_000;
178:             emit GameAndLiquidityPoolConnected(game, currency, liquidityPool);
179:         } else {
180:             bytes32 requestId = keccak256(abi.encodePacked(game, currency, liquidityPool));
181:             gameLiquidityPoolConnectionRequests[requestId] = block.timestamp;
182:             emit GameAndLiquidityPoolConnectionRequestInitiated(game, currency, liquidityPool);
183:         }
184:     }

['189']

189:     function confirmGameLiquidityPoolConnectionRequest(
190:         address game,
191:         address currency,
192:         address liquidityPool
193:     ) external onlyOwner {
194:         bytes32 requestId = keccak256(abi.encodePacked(game, currency, liquidityPool));
195:         uint256 requestedAt = gameLiquidityPoolConnectionRequests[requestId];
196: 
197:         if (requestedAt == 0) {
198:             revert GameConfigurationManager__NoGameLiquidityPoolConnectionRequest(); // <= FOUND
199:         }
200: 
201:         if (block.timestamp - requestedAt < GAME_LIQUIDITY_POOL_CONNECTION_TIMELOCK) {
202:             revert GameConfigurationManager__GameLiquidityPoolConnectionRequestConfirmationIsTooEarly(); // <= FOUND
203:         }
204: 
205:         gameLiquidityPool[game][currency] = liquidityPool;
206:         kellyFractionBasisPoints[game] = 10_000;
207: 
208:         emit GameAndLiquidityPoolConnected(game, currency, liquidityPool);
209:     }

['225']

225:     function setElapsedTimeRequiredForRefund(uint40 _elapsedTimeRequiredForRefund) external onlyOwner {
226:         if (_elapsedTimeRequiredForRefund < 10 minutes) {
227:             revert GameConfigurationManager__ElapsedTimeRequiredForRefundIsTooShort(); // <= FOUND
228:         }
229: 
230:         if (_elapsedTimeRequiredForRefund > 1 days) {
231:             revert GameConfigurationManager__ElapsedTimeRequiredForRefundIsTooLong(); // <= FOUND
232:         }
233:         elapsedTimeRequiredForRefund = _elapsedTimeRequiredForRefund;
234: 
235:         emit ElapsedTimeRequiredForRefundUpdated(_elapsedTimeRequiredForRefund);
236:     }

['250']

250:     function setGameKellyFractionBasisPoints(address game, uint256 basisPoints) external onlyOwner {
251:         if (basisPoints > 10_000) {
252:             revert GameConfigurationManager__BasisPointsTooHigh(); // <= FOUND
253:         }
254:         kellyFractionBasisPoints[game] = basisPoints;
255:         emit GameKellyFractionBasisPointsUpdated(game, basisPoints);
256:     }

['293']

293:     function setFeeSplit(
294:         address _game,
295:         uint16 _protocolFeeBasisPoints,
296:         uint16 _liquidityPoolFeeBasisPoints
297:     ) external onlyOwner {
298:         if (_protocolFeeBasisPoints + _liquidityPoolFeeBasisPoints > 500) {
299:             revert GameConfigurationManager__BasisPointsTooHigh(); // <= FOUND
300:         }
301: 
302:         feeSplit[_game] = FeeSplit({
303:             protocolFeeBasisPoints: _protocolFeeBasisPoints,
304:             liquidityPoolFeeBasisPoints: _liquidityPoolFeeBasisPoints
305:         });
306: 
307:         emit FeeSplitUpdated(_game, _protocolFeeBasisPoints, _liquidityPoolFeeBasisPoints);
308:     }

['313']

313:     function transferPayoutToPlayer(address currency, uint256 amount, address receiver) external {
314:         address liquidityPool = gameLiquidityPool[msg.sender][currency];
315:         if (liquidityPool == address(0)) {
316:             revert GameConfigurationManager__GameIsNotAllowed(msg.sender, currency); // <= FOUND
317:         }
318: 
319:         ILiquidityPool(liquidityPool).transferPayoutToPlayer(msg.sender, amount, receiver);
320:     }

['325']

325:     function transferProtocolFee(address currency, uint256 amount) external {
326:         address liquidityPool = gameLiquidityPool[msg.sender][currency];
327:         if (liquidityPool == address(0)) {
328:             revert GameConfigurationManager__GameIsNotAllowed(msg.sender, currency); // <= FOUND
329:         }
330: 
331:         ILiquidityPool(liquidityPool).transferProtocolFee(msg.sender, amount, protocolFeeRecipient);
332:     }

['117']

117:     function setMultipliers(uint128 riskLevel, uint128 rowCount, uint256[] memory _multipliers) external onlyOwner {
118:         _validateRiskLevel(riskLevel);
119:         _validateRowCount(rowCount);
120: 
121:         uint256 binsCount = _multipliers.length;
122: 
123:         if (binsCount != rowCount + 1) {
124:             revert LaserBlast__BinsCountMustBeOneHigherThanRowCount(); // <= FOUND
125:         }
126: 
127:         for (uint256 i; i < binsCount; ++i) {
128:             uint256 multiplier = _multipliers[i];
129:             if (multiplier == 0) {
130:                 revert Game__ZeroMultiplier(); // <= FOUND
131:             }
132: 
133:             bytes32 key = _multiplierKey(riskLevel, rowCount, i);
134: 
135:             if (multipliers[key] != 0) {
136:                 revert LaserBlast__MultiplierAlreadySet(); // <= FOUND
137:             }
138: 
139:             multipliers[key] = multiplier;
140:         }
141: 
142:         emit LaserBlast__MultipliersSet(riskLevel, rowCount, _multipliers);
143:     }

['155']

155:     function play(
156:         uint16 numberOfRounds,
157:         uint256 playAmountPerRound,
158:         address currency,
159:         int256 stopGain,
160:         int256 stopLoss,
161:         uint128 riskLevel,
162:         uint128 rowCount
163:     ) external payable nonReentrant {
164:         _validateNumberOfRoundsAndPlayAmountPerRound(numberOfRounds, playAmountPerRound);
165:         _validateNoOngoingRound(games[msg.sender].params.numberOfRounds);
166:         _validateStopGainAndLoss(stopGain, stopLoss);
167:         _validateMultiplierIsSet(riskLevel, rowCount);
168: 
169:         uint256 _maxPlayAmountPerGame = maxPlayAmountPerGame(currency, riskLevel, rowCount);
170:         uint256 totalPlayAmount = playAmountPerRound * numberOfRounds;
171: 
172:         if (totalPlayAmount > _maxPlayAmountPerGame) {
173:             revert Game__PlayAmountPerRoundTooHigh(); // <= FOUND
174:         }
175: 
176:         if (totalPlayAmount < _minPlayAmountPerGame(_maxPlayAmountPerGame)) {
177:             revert Game__PlayAmountPerRoundTooLow(); // <= FOUND
178:         }
179: 
180:         
181:         
182:         
183:         _getGameLiquidityPool(currency);
184: 
185:         uint256 vrfFee = _requestRandomness();
186: 
187:         _escrowPlayAmountAndChargeVrfFee(currency, numberOfRounds, playAmountPerRound, vrfFee);
188: 
189:         games[msg.sender] = LaserBlast__Game({
190:             params: Game__GameParams({
191:                 blockNumber: uint40(block.number),
192:                 numberOfRounds: numberOfRounds,
193:                 playAmountPerRound: playAmountPerRound,
194:                 currency: currency,
195:                 stopGain: stopGain,
196:                 stopLoss: stopLoss,
197:                 randomnessRequestedAt: uint40(block.timestamp),
198:                 vrfFee: vrfFee
199:             }),
200:             riskLevel: riskLevel,
201:             rowCount: rowCount
202:         });
203: 
204:         emit LaserBlast__GameCreated(
205:             block.number,
206:             msg.sender,
207:             numberOfRounds,
208:             playAmountPerRound,
209:             currency,
210:             stopGain,
211:             stopLoss,
212:             riskLevel,
213:             rowCount
214:         );
215:     }

['228']

228:     function addLiquidityPool(address liquidityPool) external onlyOwner {
229:         address token = ERC4626(liquidityPool).asset();
230:         if (liquidityPools[token] != address(0)) {
231:             revert LiquidityPoolRouter__TokenAlreadyHasLiquidityPool(); // <= FOUND
232:         }
233:         liquidityPools[token] = liquidityPool;
234:         emit LiquidityPoolRouter__LiquidityPoolAdded(token, liquidityPool);
235:     }

['245']

245:     function setDepositLimit(
246:         address liquidityPool,
247:         uint256 minDepositAmount,
248:         uint256 maxDepositAmount,
249:         uint256 maxBalance
250:     ) external onlyOwner {
251:         if (minDepositAmount > maxDepositAmount) {
252:             revert LiquidityPoolRouter__MinDepositAmountTooHigh(); // <= FOUND
253:         }
254: 
255:         if (maxBalance < maxDepositAmount) {
256:             revert LiquidityPoolRouter__MaxDepositAmountTooHigh(); // <= FOUND
257:         }
258: 
259:         depositLimit[liquidityPool] = DepositLimit(minDepositAmount, maxDepositAmount, maxBalance);
260:         emit LiquidityPoolRouter__DepositLimitUpdated(liquidityPool, minDepositAmount, maxDepositAmount, maxBalance);
261:     }

['284']

284:     function depositETH(uint256 amount) external payable nonReentrant whenNotPaused {
285:         address liquidityPool = _getLiquidityPoolOrRevert(WETH);
286: 
287:         if (amount + finalizationParams.finalizationIncentive != msg.value) {
288:             revert LiquidityPoolRouter__FinalizationIncentiveNotPaid(); // <= FOUND
289:         }
290: 
291:         uint256 depositFee = (amount * DEPOSIT_FEE_BASIS_POINTS) / 10_000;
292:         amount -= depositFee;
293: 
294:         _validateDepositAmount(liquidityPool, amount);
295: 
296:         if (deposits[msg.sender].amount != 0) {
297:             revert LiquidityPoolRouter__OngoingDeposit(); // <= FOUND
298:         }
299: 
300:         _transferETHAndWrapIfFailWithGasLimit(WETH, owner, depositFee, gasleft());
301: 
302:         uint256 expectedShares = ERC4626(liquidityPool).previewDeposit(amount);
303: 
304:         deposits[msg.sender] = Deposit(
305:             liquidityPool,
306:             amount,
307:             expectedShares,
308:             block.timestamp,
309:             finalizationParams.finalizationIncentive
310:         );
311:         pendingDeposits[liquidityPool] += amount;
312: 
313:         emit LiquidityPoolRouter__DepositInitialized(
314:             msg.sender,
315:             liquidityPool,
316:             amount + depositFee,
317:             expectedShares,
318:             finalizationParams.finalizationIncentive
319:         );
320:     }

['329']

329:     function deposit(address token, uint256 amount) external payable nonReentrant whenNotPaused {
330:         address liquidityPool = _getLiquidityPoolOrRevert(token);
331: 
332:         _validateFinalizationIncentivePayment();
333: 
334:         uint256 depositFee = (amount * DEPOSIT_FEE_BASIS_POINTS) / 10_000;
335:         amount -= depositFee;
336: 
337:         _validateDepositAmount(liquidityPool, amount);
338: 
339:         if (deposits[msg.sender].amount != 0) {
340:             revert LiquidityPoolRouter__OngoingDeposit(); // <= FOUND
341:         }
342: 
343:         TRANSFER_MANAGER.transferERC20(token, msg.sender, address(this), amount);
344:         TRANSFER_MANAGER.transferERC20(token, msg.sender, owner, depositFee);
345: 
346:         uint256 expectedShares = ERC4626(liquidityPool).previewDeposit(amount);
347: 
348:         deposits[msg.sender] = Deposit(
349:             liquidityPool,
350:             amount,
351:             expectedShares,
352:             block.timestamp,
353:             finalizationParams.finalizationIncentive
354:         );
355:         pendingDeposits[liquidityPool] += amount;
356: 
357:         emit LiquidityPoolRouter__DepositInitialized(
358:             msg.sender,
359:             liquidityPool,
360:             amount + depositFee,
361:             expectedShares,
362:             finalizationParams.finalizationIncentive
363:         );
364:     }

['374']

374:     function finalizeDeposit(address depositor) external nonReentrant {
375:         uint256 amount = deposits[depositor].amount;
376:         if (amount == 0) {
377:             revert LiquidityPoolRouter__NoOngoingDeposit(); // <= FOUND
378:         }
379: 
380:         uint256 initializedAt = deposits[depositor].initializedAt;
381:         _validateTimelockIsOver(initializedAt);
382:         _validateFinalizationIsOpenForAll(depositor, initializedAt);
383: 
384:         address payable liquidityPool = payable(deposits[depositor].liquidityPool);
385:         address token = ERC4626(liquidityPool).asset();
386:         uint256 expectedShares = deposits[depositor].expectedShares;
387:         uint256 actualShares = ERC4626(liquidityPool).previewDeposit(amount);
388:         uint256 incentive = deposits[depositor].finalizationIncentive;
389: 
390:         deposits[depositor] = Deposit(address(0), 0, 0, 0, 0);
391:         pendingDeposits[liquidityPool] -= amount;
392: 
393:         _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, incentive, 2_300);
394: 
395:         uint256 sharesMinted;
396:         uint256 amountRequired;
397:         if (expectedShares >= actualShares) {
398:             amountRequired = amount;
399:             sharesMinted = _deposit(token, liquidityPool, amountRequired, depositor);
400:         } else {
401:             amountRequired = ERC4626(liquidityPool).previewMint(expectedShares);
402:             sharesMinted = _deposit(token, liquidityPool, amountRequired, depositor);
403:             if (token == WETH) {
404:                 _transferETHAndWrapIfFailWithGasLimit(WETH, liquidityPool, amount - amountRequired, gasleft());
405:             } else {
406:                 _executeERC20DirectTransfer(token, liquidityPool, amount - amountRequired);
407:             }
408:         }
409: 
410:         emit LiquidityPoolRouter__DepositFinalized(msg.sender, depositor, liquidityPool, amountRequired, sharesMinted);
411:     }

['420']

420:     function redeem(address token, uint256 amount) external payable nonReentrant {
421:         address liquidityPool = _getLiquidityPoolOrRevert(token);
422: 
423:         _validateFinalizationIncentivePayment();
424: 
425:         if (redemptions[msg.sender].shares != 0) {
426:             revert LiquidityPoolRouter__OngoingRedemption(); // <= FOUND
427:         }
428: 
429:         TRANSFER_MANAGER.transferERC20(liquidityPool, msg.sender, address(this), amount);
430: 
431:         uint256 expectedAssets = ERC4626(liquidityPool).previewRedeem(amount);
432: 
433:         redemptions[msg.sender] = Redemption(
434:             liquidityPool,
435:             amount,
436:             expectedAssets,
437:             block.timestamp,
438:             finalizationParams.finalizationIncentive
439:         );
440: 
441:         emit LiquidityPoolRouter__RedemptionInitialized(
442:             msg.sender,
443:             liquidityPool,
444:             amount,
445:             expectedAssets,
446:             finalizationParams.finalizationIncentive
447:         );
448:     }

['458']

458:     function finalizeRedemption(address redeemer) external nonReentrant {
459:         uint256 amount = redemptions[redeemer].shares;
460:         if (amount == 0) {
461:             revert LiquidityPoolRouter__NoOngoingRedemption(); // <= FOUND
462:         }
463: 
464:         uint256 initializedAt = redemptions[redeemer].initializedAt;
465:         _validateTimelockIsOver(initializedAt);
466:         _validateFinalizationIsOpenForAll(redeemer, initializedAt);
467: 
468:         address payable liquidityPool = payable(redemptions[redeemer].liquidityPool);
469:         address token = ERC4626(liquidityPool).asset();
470:         uint256 expectedAssets = redemptions[redeemer].expectedAssets;
471:         uint256 incentive = redemptions[redeemer].finalizationIncentive;
472: 
473:         redemptions[redeemer] = Redemption(address(0), 0, 0, 0, 0);
474: 
475:         uint256 assetsRedeemed = LiquidityPool(liquidityPool).redeem(amount, address(this), address(this));
476: 
477:         _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, incentive, 2_300);
478: 
479:         if (expectedAssets >= assetsRedeemed) {
480:             _transferAssetsRedeemed(token, redeemer, assetsRedeemed);
481:         } else {
482:             _transferAssetsRedeemed(token, redeemer, expectedAssets);
483:             _executeERC20DirectTransfer(token, liquidityPool, assetsRedeemed - expectedAssets);
484:             assetsRedeemed = expectedAssets;
485:         }
486: 
487:         emit LiquidityPoolRouter__RedemptionFinalized(msg.sender, redeemer, liquidityPool, amount, assetsRedeemed);
488:     }

['95']

95:     function play(
96:         uint16 numberOfRounds,
97:         uint256 playAmountPerRound,
98:         address currency,
99:         int256 stopGain,
100:         int256 stopLoss,
101:         bool isAbove,
102:         uint248 multiplier
103:     ) external payable nonReentrant {
104:         _validateNumberOfRoundsAndPlayAmountPerRound(numberOfRounds, playAmountPerRound);
105:         _validateNoOngoingRound(games[msg.sender].params.numberOfRounds);
106:         _validateStopGainAndLoss(stopGain, stopLoss);
107:         _validateMultiplier(multiplier);
108: 
109:         uint256 _maxPlayAmountPerGame = maxPlayAmountPerGame(currency, multiplier);
110:         uint256 totalPlayAmount = playAmountPerRound * numberOfRounds;
111: 
112:         if (totalPlayAmount > _maxPlayAmountPerGame) {
113:             revert Game__PlayAmountPerRoundTooHigh(); // <= FOUND
114:         }
115: 
116:         if (totalPlayAmount < _minPlayAmountPerGame(_maxPlayAmountPerGame)) {
117:             revert Game__PlayAmountPerRoundTooLow(); // <= FOUND
118:         }
119: 
120:         
121:         
122:         
123:         _getGameLiquidityPool(currency);
124: 
125:         uint256 vrfFee = _requestRandomness();
126: 
127:         _escrowPlayAmountAndChargeVrfFee(currency, numberOfRounds, playAmountPerRound, vrfFee);
128: 
129:         games[msg.sender] = Quantum__Game({
130:             params: Game__GameParams({
131:                 blockNumber: uint40(block.number),
132:                 numberOfRounds: numberOfRounds,
133:                 playAmountPerRound: playAmountPerRound,
134:                 currency: currency,
135:                 stopGain: stopGain,
136:                 stopLoss: stopLoss,
137:                 randomnessRequestedAt: uint40(block.timestamp),
138:                 vrfFee: vrfFee
139:             }),
140:             isAbove: isAbove,
141:             multiplier: multiplier
142:         });
143: 
144:         emit Quantum__GameCreated(
145:             block.number,
146:             msg.sender,
147:             numberOfRounds,
148:             playAmountPerRound,
149:             currency,
150:             stopGain,
151:             stopLoss,
152:             isAbove,
153:             multiplier
154:         );
155:     }

['426']

426:     function kellyFraction(uint256 lavasCount, uint256 selectedTilesCount) public view returns (uint256) {
427:         if (lavasCount == 0 || lavasCount >= GRID_SIZE) {
428:             revert Game__InvalidValue(); // <= FOUND
429:         }
430: 
431:         uint256 winProbability = winProbabilities[lavasCount][selectedTilesCount];
432:         uint256 multiplier = _getMultiplier(winProbability);
433:         return
434:             ((1e18 - winProbability) * KELLY_FRACTION_SCALER) /
435:             ((multiplier * _liquidityProviderAdjustedReturn()) / 10_000 - 1e4) -
436:             (winProbability * KELLY_FRACTION_SCALER) /
437:             1e4;
438:     }

['90']

90:     function mint(uint256, address) public pure override returns (uint256) {
91:         revert LiquidityPool__UnsupportedOperation(); // <= FOUND
92:     }

['97']

97:     function withdraw(uint256, address, address) public pure override returns (uint256) {
98:         revert LiquidityPool__UnsupportedOperation(); // <= FOUND
99:     }

['341']

341:     function defineBoundary(uint256 winProbability) public pure returns (uint256 boundary) {
342:         if (winProbability > TOTAL_OUTCOMES) {
343:             revert Game__InvalidValue(); // <= FOUND
344:         }
345:         boundary = TOTAL_OUTCOMES - winProbability;
346:     }

[NonCritical-10] Contract lines should not be longer than 120 characters for readability

Resolution

Consider spreading these lines over multiple lines to aid in readability and the support of VIM users everywhere.

Num of instances: 10

Findings

Click to show findings

['116']

116:      * @param _vrfCoordinator The address of the VRF coordinator for Chainlink VRF. It is set as our VRF coordinator adapter for Gelato. // <= FOUND

['88']

88:      *      This number is based on the maximum and minimum win probability of 95% and 0.1% with a house edge of 1% respectively. // <= FOUND

['211']

211:      *      On a loss (player wins), we would usually lose the entire amount, however as we take a 2% fee for our losses, we only lose 98%. // <= FOUND

['216']

216:      * @dev We don't check for duplicated request IDs because Gelato's Chainlink adapter increments the request ID up to uint256 max. // <= FOUND

['242']

242:      * @dev Add unplayed amount to the payout, transfer the total play amount to the liquidity pool, and transfer the final payout to the player. // <= FOUND

['345']

345:      * @dev Validate that there is no ongoing round. When a game is completed, the game struct is reset so it should be 0. // <= FOUND

['136']

136:      * @param finalizationIncentive The router incentivizes bots that backstop finalization if the depositor/redeemer does not complete the process // <= FOUND

['276']

276:      * @notice Return the minimum play amount per game for the given currency, risk level and row count. It must be 0.01% of the // <= FOUND

['160']

160:      *      On a loss, we would usually lose the entire amount, however as we take a 2% fee for our losses, we only lose 98%. // <= FOUND

['96']

96:      * @notice This is only meant for receiving payouts, no one should send ETH to this contract and expect to get shares. // <= FOUND

[NonCritical-11] Avoid updating storage when the value hasn't changed

Resolution

In Solidity, manipulating contract storage comes with significant gas costs. One can optimize gas usage by preventing unnecessary storage updates when the new value is the same as the existing one. If an existing value is the same as the new one, not reassigning it to the storage could potentially save substantial amounts of gas, notably 2900 gas for a 'Gsreset'. This saving may come at the expense of a cold storage load operation ('Gcoldsload'), which costs 2100 gas, or a warm storage access operation ('Gwarmaccess'), which costs 100 gas. Therefore, the gas efficiency of your contract can be significantly improved by adding a check that compares the new value with the current one before any storage update operation. If the values are the same, you can bypass the storage operation, thereby saving gas.

Num of instances: 9

Findings

Click to show findings

['225']

225:     function setElapsedTimeRequiredForRefund(uint40 _elapsedTimeRequiredForRefund) external onlyOwner {
226:         if (_elapsedTimeRequiredForRefund < 10 minutes) {
227:             revert GameConfigurationManager__ElapsedTimeRequiredForRefundIsTooShort();
228:         }
229: 
230:         if (_elapsedTimeRequiredForRefund > 1 days) {
231:             revert GameConfigurationManager__ElapsedTimeRequiredForRefundIsTooLong();
232:         }
233:         elapsedTimeRequiredForRefund = _elapsedTimeRequiredForRefund;
234: 
235:         emit ElapsedTimeRequiredForRefundUpdated(_elapsedTimeRequiredForRefund);
236:     }

['242']

242:     function setMaximumNumberOfRounds(uint16 _maximumNumberOfRounds) external onlyOwner {
243:         maximumNumberOfRounds = _maximumNumberOfRounds;
244:         emit MaximumNumberOfRoundsUpdated(_maximumNumberOfRounds);
245:     }

['250']

250:     function setGameKellyFractionBasisPoints(address game, uint256 basisPoints) external onlyOwner {
251:         if (basisPoints > 10_000) {
252:             revert GameConfigurationManager__BasisPointsTooHigh();
253:         }
254:         kellyFractionBasisPoints[game] = basisPoints;
255:         emit GameKellyFractionBasisPointsUpdated(game, basisPoints);
256:     }

['262']

262:     function setVrfParameters(VrfParameters memory _vrfParameters) external onlyOwner {
263:         _setVrfParameters(_vrfParameters);
264:     }

['270']

270:     function setVrfFeeRecipient(address _vrfFeeRecipient) external onlyOwner {
271:         vrfFeeRecipient = _vrfFeeRecipient;
272:         emit VrfFeeRecipientUpdated(_vrfFeeRecipient);
273:     }

['279']

279:     function setProtocolFeeRecipient(address _protocolFeeRecipient) external onlyOwner {
280:         protocolFeeRecipient = _protocolFeeRecipient;
281:         emit ProtocolFeeRecipientUpdated(_protocolFeeRecipient);
282:     }

['293']

293:     function setFeeSplit(
294:         address _game,
295:         uint16 _protocolFeeBasisPoints,
296:         uint16 _liquidityPoolFeeBasisPoints
297:     ) external onlyOwner {
298:         if (_protocolFeeBasisPoints + _liquidityPoolFeeBasisPoints > 500) {
299:             revert GameConfigurationManager__BasisPointsTooHigh();
300:         }
301: 
302:         feeSplit[_game] = FeeSplit({
303:             protocolFeeBasisPoints: _protocolFeeBasisPoints,
304:             liquidityPoolFeeBasisPoints: _liquidityPoolFeeBasisPoints
305:         });
306: 
307:         emit FeeSplitUpdated(_game, _protocolFeeBasisPoints, _liquidityPoolFeeBasisPoints);
308:     }

['245']

245:     function setDepositLimit(
246:         address liquidityPool,
247:         uint256 minDepositAmount,
248:         uint256 maxDepositAmount,
249:         uint256 maxBalance
250:     ) external onlyOwner {
251:         if (minDepositAmount > maxDepositAmount) {
252:             revert LiquidityPoolRouter__MinDepositAmountTooHigh();
253:         }
254: 
255:         if (maxBalance < maxDepositAmount) {
256:             revert LiquidityPoolRouter__MaxDepositAmountTooHigh();
257:         }
258: 
259:         depositLimit[liquidityPool] = DepositLimit(minDepositAmount, maxDepositAmount, maxBalance);
260:         emit LiquidityPoolRouter__DepositLimitUpdated(liquidityPool, minDepositAmount, maxDepositAmount, maxBalance);
261:     }

['270']

270:     function setFinalizationParams(
271:         uint80 _timelockDelay,
272:         uint80 _finalizationForAllDelay,
273:         uint80 _finalizationIncentive
274:     ) external onlyOwner {
275:         _setFinalizationParams(_timelockDelay, _finalizationForAllDelay, _finalizationIncentive);
276:     }

[NonCritical-12] Not all event definitions are utilizing indexed variables.

Resolution

Try to index as much as three variables in event declarations as this is more gas efficient when done on value type variables (uint, address etc) however not for bytes and string variables

Num of instances: 32

Findings

Click to show findings

['75']

75: event DontFallIn__GameCreated( // <= FOUND
76:         uint256 blockNumber,
77:         address player,
78:         uint256 playAmountPerRound,
79:         address currency,
80:         uint256 lavasCount,
81:         uint256 selectedTiles,
82:         bool cashoutIfWon
83:     );

['84']

84: event DontFallIn__GameContinued(uint256 blockNumber, address player, uint256 selectedTiles, bool cashoutIfWon); // <= FOUND

['86']

86: event DontFallIn__GameWon( // <= FOUND
87:         uint256 blockNumber,
88:         address player,
89:         uint256 selectedTiles,
90:         uint256 payout,
91:         uint256 multiplier,
92:         uint256 protocolFee,
93:         uint256 liquidityPoolFee
94:     );

['95']

95: event DontFallIn__GamePlayed(uint256 blockNumber, address player, uint256 selectedTiles, uint256 currentMultiplier); // <= FOUND

['96']

96: event DontFallIn__GameLost(uint256 blockNumber, address player, uint256 selectedTiles, uint256 lavas); // <= FOUND

['98']

98: event DontFallIn__GameCashedOut( // <= FOUND
99:         uint256 blockNumber,
100:         address player,
101:         uint256 payout,
102:         uint256 multiplier,
103:         uint256 protocolFee,
104:         uint256 liquidityPoolFee
105:     );

['106']

106: event DontFallIn__GameCashedOutOriginalAmount(uint256 blockNumber, address player, uint256 originalAmount); // <= FOUND

['26']

26: event Flipper__GameCreated( // <= FOUND
27:         uint256 blockNumber,
28:         address player,
29:         uint256 numberOfRounds,
30:         uint256 playAmountPerRound,
31:         address currency,
32:         int256 stopGain,
33:         int256 stopLoss,
34:         bool isGold
35:     );

['37']

37: event Flipper__GameCompleted( // <= FOUND
38:         uint256 blockNumber,
39:         address player,
40:         bool[] results,
41:         uint256[] payouts,
42:         uint256 numberOfRoundsPlayed,
43:         uint256 protocolFee,
44:         uint256 liquidityPoolFee
45:     );

['88']

88: event ElapsedTimeRequiredForRefundUpdated(uint256 _elapsedTimeRequiredForRefund); // <= FOUND

['89']

89: event FeeSplitUpdated(address game, uint256 protocolFeeBasisPoints, uint256 liquidityPoolFeeBasisPoints); // <= FOUND

['90']

90: event VrfFeeRecipientUpdated(address _vrfFeeRecipient); // <= FOUND

['91']

91: event GameAndLiquidityPoolConnected(address game, address currency, address liquidityPool); // <= FOUND

['92']

92: event GameAndLiquidityPoolConnectionRequestInitiated(address game, address currency, address liquidityPool); // <= FOUND

['93']

93: event GameAndLiquidityPoolDisconnected(address game, address currency, address liquidityPool); // <= FOUND

['94']

94: event GameKellyFractionBasisPointsUpdated(address game, uint256 basisPoints); // <= FOUND

['95']

95: event MaximumNumberOfRoundsUpdated(uint256 _maximumNumberOfRounds); // <= FOUND

['96']

96: event ProtocolFeeRecipientUpdated(address _protocolFeeRecipient); // <= FOUND

['97']

97: event VrfParametersUpdated( // <= FOUND
98:         address coordinator,
99:         uint64 subscriptionId,
100:         uint32 callbackGasLimit,
101:         uint16 minimumRequestConfirmations,
102:         uint240 vrfFee,
103:         bytes32 keyHash
104:     );

['58']

58: event LaserBlast__GameCreated( // <= FOUND
59:         uint256 blockNumber,
60:         address player,
61:         uint256 numberOfRounds,
62:         uint256 playAmountPerRound,
63:         address currency,
64:         int256 stopGain,
65:         int256 stopLoss,
66:         uint256 riskLevel,
67:         uint256 rowCount
68:     );

['70']

70: event LaserBlast__GameCompleted( // <= FOUND
71:         uint256 blockNumber,
72:         address player,
73:         uint256[] results,
74:         uint256[] payouts,
75:         uint256 numberOfRoundsPlayed,
76:         uint256 protocolFee,
77:         uint256 liquidityPoolFee
78:     );

['79']

79: event LaserBlast__MultipliersSet(uint256 riskLevel, uint256 rowCount, uint256[] multipliers); // <= FOUND

['29']

29: event LiquidityPoolRouter__DepositInitialized( // <= FOUND
30:         address user,
31:         address liquidityPool,
32:         uint256 amount,
33:         uint256 expectedShares,
34:         uint256 finalizationIncentive
35:     );

['36']

36: event LiquidityPoolRouter__DepositFinalized( // <= FOUND
37:         address caller,
38:         address user,
39:         address liquidityPool,
40:         uint256 amount,
41:         uint256 sharesMinted
42:     );

['43']

43: event LiquidityPoolRouter__DepositLimitUpdated( // <= FOUND
44:         address liquidityPool,
45:         uint256 minDepositAmount,
46:         uint256 maxDepositAmount,
47:         uint256 maxBalance
48:     );

['49']

49: event LiquidityPoolRouter__FinalizationParamsUpdated( // <= FOUND
50:         uint256 timelockDelay,
51:         uint256 finalizationForAllDelay,
52:         uint256 finalizationIncentive
53:     );

['54']

54: event LiquidityPoolRouter__LiquidityPoolAdded(address token, address liquidityPool); // <= FOUND

['55']

55: event LiquidityPoolRouter__RedemptionInitialized( // <= FOUND
56:         address user,
57:         address liquidityPool,
58:         uint256 amount,
59:         uint256 expectedAssets,
60:         uint256 finalizationIncentive
61:     );

['62']

62: event LiquidityPoolRouter__RedemptionFinalized( // <= FOUND
63:         address caller,
64:         address user,
65:         address liquidityPool,
66:         uint256 amount,
67:         uint256 assetsRedeemed
68:     );

['34']

34: event Quantum__GameCreated( // <= FOUND
35:         uint256 blockNumber,
36:         address player,
37:         uint256 numberOfRounds,
38:         uint256 playAmountPerRound,
39:         address currency,
40:         int256 stopGain,
41:         int256 stopLoss,
42:         bool isAbove,
43:         uint256 multiplier
44:     );

['46']

46: event Quantum__GameCompleted( // <= FOUND
47:         uint256 blockNumber,
48:         address player,
49:         uint256[] results,
50:         uint256[] payouts,
51:         uint256 numberOfRoundsPlayed,
52:         uint256 protocolFee,
53:         uint256 liquidityPoolFee
54:     );

['88']

88: event Game__Refunded(uint256 blockNumber, address player, uint256 totalPlayAmount); // <= FOUND

[NonCritical-13] Contracts should have all public/external functions exposed by interfaces

Resolution

Contracts should expose all public and external functions through interfaces. This practice ensures a clear and consistent definition of how the contract can be interacted with, promoting better transparency and integration.

Num of instances: 56

Findings

Click to show findings

['168']

168:     function play(
169:         uint256 playAmountPerRound,
170:         address currency,
171:         uint8 lavasCount,
172:         uint32 selectedTiles,
173:         bool cashoutIfWon
174:     ) external payable nonReentrant 

['242']

242:     function playOngoing(uint32 selectedTiles, bool cashoutIfWon) external payable nonReentrant 

['289']

289:     function cashout() external nonReentrant 

['333']

333:     function cashoutOriginalAmount() external nonReentrant 

['365']

365:     function refund() external nonReentrant 

['447']

447:     function getMultiplier(uint256 lavasCount, uint256 revealedTilesCount) external view returns (uint256 multiplier) 

['61']

61:     function transferPayoutToPlayer(
62:         address game,
63:         uint256 amount,
64:         address receiver
65:     ) external nonReentrant whenNotPaused 

['80']

80:     function transferProtocolFee(
81:         address game,
82:         uint256 amount,
83:         address protocolFeeRecipient
84:     ) external nonReentrant whenNotPaused 

['61']

61:     function transferPayoutToPlayer(
62:         address game,
63:         uint256 amount,
64:         address receiver
65:     ) external nonReentrant whenNotPaused 

['80']

80:     function transferProtocolFee(
81:         address game,
82:         uint256 amount,
83:         address protocolFeeRecipient
84:     ) external nonReentrant whenNotPaused 

['75']

75:     function play(
76:         uint16 numberOfRounds,
77:         uint256 playAmountPerRound,
78:         address currency,
79:         int256 stopGain,
80:         int256 stopLoss,
81:         bool isGold
82:     ) external payable nonReentrant 

['365']

365:     function refund() external nonReentrant 

['112']

112:     function claimYield(address receiver) external onlyOwner 

['164']

164:     function initiateGameLiquidityPoolConnectionRequest(
165:         address game,
166:         address currency,
167:         address liquidityPool
168:     ) external onlyOwner 

['189']

189:     function confirmGameLiquidityPoolConnectionRequest(
190:         address game,
191:         address currency,
192:         address liquidityPool
193:     ) external onlyOwner 

['214']

214:     function disconnectGameFromLiquidityPool(address game, address currency) external onlyOwner 

['225']

225:     function setElapsedTimeRequiredForRefund(uint40 _elapsedTimeRequiredForRefund) external onlyOwner 

['242']

242:     function setMaximumNumberOfRounds(uint16 _maximumNumberOfRounds) external onlyOwner 

['250']

250:     function setGameKellyFractionBasisPoints(address game, uint256 basisPoints) external onlyOwner 

['262']

262:     function setVrfParameters(VrfParameters memory _vrfParameters) external onlyOwner 

['270']

270:     function setVrfFeeRecipient(address _vrfFeeRecipient) external onlyOwner 

['279']

279:     function setProtocolFeeRecipient(address _protocolFeeRecipient) external onlyOwner 

['293']

293:     function setFeeSplit(
294:         address _game,
295:         uint16 _protocolFeeBasisPoints,
296:         uint16 _liquidityPoolFeeBasisPoints
297:     ) external onlyOwner 

['313']

313:     function transferPayoutToPlayer(address currency, uint256 amount, address receiver) external 

['325']

325:     function transferProtocolFee(address currency, uint256 amount) external 

['337']

337:     function getGameLiquidityPool(address game, address currency) external view returns (address liquidityPool) 

['117']

117:     function setMultipliers(uint128 riskLevel, uint128 rowCount, uint256[] memory _multipliers) external onlyOwner 

['155']

155:     function play(
156:         uint16 numberOfRounds,
157:         uint256 playAmountPerRound,
158:         address currency,
159:         int256 stopGain,
160:         int256 stopLoss,
161:         uint128 riskLevel,
162:         uint128 rowCount
163:     ) external payable nonReentrant 

['365']

365:     function refund() external nonReentrant 

['231']

231:     function getMultipliers(uint128 riskLevel, uint128 rowCount) external view returns (uint256[] memory _multipliers) 

['104']

104:     function togglePaused() external onlyOwner 

['112']

112:     function claimYield(address receiver) external onlyOwner 

['228']

228:     function addLiquidityPool(address liquidityPool) external onlyOwner 

['245']

245:     function setDepositLimit(
246:         address liquidityPool,
247:         uint256 minDepositAmount,
248:         uint256 maxDepositAmount,
249:         uint256 maxBalance
250:     ) external onlyOwner 

['270']

270:     function setFinalizationParams(
271:         uint80 _timelockDelay,
272:         uint80 _finalizationForAllDelay,
273:         uint80 _finalizationIncentive
274:     ) external onlyOwner 

['284']

284:     function depositETH(uint256 amount) external payable nonReentrant whenNotPaused 

['329']

329:     function deposit(address token, uint256 amount) external payable nonReentrant whenNotPaused 

['374']

374:     function finalizeDeposit(address depositor) external nonReentrant 

['420']

420:     function redeem(address token, uint256 amount) external payable nonReentrant 

['458']

458:     function finalizeRedemption(address redeemer) external nonReentrant 

['112']

112:     function claimYield(address receiver) external onlyOwner 

['95']

95:     function play(
96:         uint16 numberOfRounds,
97:         uint256 playAmountPerRound,
98:         address currency,
99:         int256 stopGain,
100:         int256 stopLoss,
101:         bool isAbove,
102:         uint248 multiplier
103:     ) external payable nonReentrant 

['365']

365:     function refund() external nonReentrant 

['381']

381:     function maxPlayAmountPerGame(
382:         address currency,
383:         uint256 lavasCount,
384:         uint256 selectedTilesCount
385:     ) public view returns (uint256 maxPlayAmount) 

['409']

409:     function minPlayAmountPerGame(
410:         address currency,
411:         uint256 lavasCount,
412:         uint256 selectedTilesCount
413:     ) public view returns (uint256 minPlayAmount) 

['426']

426:     function kellyFraction(uint256 lavasCount, uint256 selectedTilesCount) public view returns (uint256) 

['167']

167:     function maxPlayAmountPerGame(address currency) public view returns (uint256 maxPlayAmount) 

['181']

181:     function minPlayAmountPerGame(address currency) public view returns (uint256 minPlayAmount) 

['188']

188:     function kellyFraction() public view returns (uint256) 

['254']

254:     function maxPlayAmountPerGame(
255:         address currency,
256:         uint128 riskLevel,
257:         uint128 rowCount
258:     ) public view returns (uint256 maxPlayAmount) 

['282']

282:     function minPlayAmountPerGame(
283:         address currency,
284:         uint128 riskLevel,
285:         uint128 rowCount
286:     ) public view returns (uint256 minPlayAmount) 

['176']

176:     function maxPlayAmountPerGame(address currency, uint256 multiplier) public view returns (uint256 maxPlayAmount) 

['193']

193:     function minPlayAmountPerGame(address currency, uint256 multiplier) public view returns (uint256 minPlayAmount) 

['223']

223:     function kellyFraction(uint256 multiplier) public view returns (uint256) 

['333']

333:     function calculateWinProbability(uint256 multiplier) public pure returns (uint256 winProbability) 

['341']

341:     function defineBoundary(uint256 winProbability) public pure returns (uint256 boundary) 

[NonCritical-14] Functions within contracts are not ordered according to the solidity style guide

Resolution

The following order should be used within contracts

constructor

receive function (if exists)

fallback function (if exists)

external

public

internal

private

Rearrange the contract functions and contructors to fit this ordering

Num of instances: 3

Findings

Click to show findings

['13']

13: contract DontFallIn is Game  // <= FOUND

['13']

13: contract Quantum is Game  // <= FOUND

[]

24: abstract contract LiquidityPool is
25:     ERC4626,
26:     OwnableTwoSteps,
27:     ReentrancyGuard,
28:     Pausable,
29:     ILiquidityPool,
30:     LowLevelERC20Transfer
31: 

[NonCritical-15] Functions with array parameters should have length checks in place

Resolution

Functions in Solidity that accept array parameters should incorporate length checks as a security measure. This is to prevent potential overflow errors, unwanted gas consumption, and manipulation attempts. Without length checks, an attacker could pass excessively large arrays as input, causing excessive computation and potentially causing the function to exceed the block gas limit, leading to a denial-of-service. Additionally, unexpected array sizes could lead to logic errors within the function. As a resolution, always validate array length at the start of functions handling array inputs, ensuring it aligns with the expectations of the function logic. This makes the code more robust and predictable.

Num of instances: 4

Findings

Click to show findings

['459']

459:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
460:         address player = randomnessRequests[requestId];
461:         if (player != address(0)) {
462:             DontFallIn__Game storage game = games[player];
463:             if (_hasLiquidityPool(game.params.currency)) {
464:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) {
465:                     randomnessRequests[requestId] = address(0);
466: 
467:                     RunningGameState memory runningGameState;
468:                     runningGameState.randomWord = randomWords[0];
469: 
470:                     uint256 lavas;
471:                     (uint256 selectedTilesCount, uint256[] memory indices) = _nonZeroTilesCount(game.selectedTiles); // <= FOUND
472:                     uint32 grid = game.grid;
473:                     (uint256 revealedTilesCount, ) = _nonZeroTilesCount(grid);
474:                     for (uint256 i; i < selectedTilesCount; ++i) {
475:                         if (
476:                             runningGameState.randomWord % 10_000 <
477:                             (game.lavasCount * 10_000) / (GRID_SIZE - revealedTilesCount)
478:                         ) {
479:                             lavas |= 1 << indices[i];
480:                         } else {
481:                             grid |= uint32(1 << indices[i]);
482:                         }
483: 
484:                         unchecked {
485:                             ++revealedTilesCount;
486:                         }
487: 
488:                         runningGameState.randomWord = uint256(keccak256(abi.encode(runningGameState.randomWord)));
489:                     }
490: 
491:                     _transferVrfFee(game.params.vrfFee);
492: 
493:                     if (lavas == 0) {
494:                         IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
495:                             address(this)
496:                         );
497: 
498:                         uint256 multiplier = _getMultiplier(winProbabilities[game.lavasCount][revealedTilesCount]);
499:                         if (game.cashoutIfWon) {
500:                             Fee memory fee = Fee({
501:                                 protocolFee: (game.params.playAmountPerRound *
502:                                     multiplier *
503:                                     feeSplit.protocolFeeBasisPoints) / 1e8,
504:                                 liquidityPoolFee: (game.params.playAmountPerRound *
505:                                     multiplier *
506:                                     feeSplit.liquidityPoolFeeBasisPoints) / 1e8
507:                             });
508:                             runningGameState.payout =
509:                                 (game.params.playAmountPerRound * multiplier) /
510:                                 10_000 -
511:                                 fee.protocolFee -
512:                                 fee.liquidityPoolFee;
513:                             _handlePayout(player, game.params, 1, runningGameState.payout, fee.protocolFee);
514:                             emit DontFallIn__GameWon(
515:                                 game.params.blockNumber,
516:                                 player,
517:                                 game.selectedTiles,
518:                                 runningGameState.payout,
519:                                 multiplier,
520:                                 fee.protocolFee,
521:                                 fee.liquidityPoolFee
522:                             );
523:                             _deleteGame(player);
524:                         } else {
525:                             game.params.randomnessRequestedAt = 0;
526:                             game.params.vrfFee = 0;
527:                             game.grid = grid;
528:                             game.multiplier = uint176(multiplier);
529: 
530:                             emit DontFallIn__GamePlayed(
531:                                 game.params.blockNumber,
532:                                 player,
533:                                 game.selectedTiles,
534:                                 game.multiplier
535:                             );
536:                         }
537:                     } else {
538:                         _handlePayout(player, game.params, 1, 0, 0);
539:                         emit DontFallIn__GameLost(game.params.blockNumber, player, game.selectedTiles, lavas);
540:                         _deleteGame(player);
541:                     }
542:                 }
543:             }
544:         }
545:     }

['198']

198:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
199:         address player = randomnessRequests[requestId];
200: 
201:         if (player != address(0)) {
202:             Flipper__Game storage game = games[player];
203:             if (_hasLiquidityPool(game.params.currency)) {
204:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) {
205:                     randomnessRequests[requestId] = address(0);
206: 
207:                     RunningGameState memory runningGameState;
208:                     runningGameState.randomWord = randomWords[0];
209:                     runningGameState.payouts = new uint256[](game.params.numberOfRounds); // <= FOUND
210:                     bool[] memory results = new bool[](game.params.numberOfRounds); // <= FOUND
211: 
212:                     IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
213:                         address(this)
214:                     );
215:                     Fee memory fee;
216: 
217:                     for (
218:                         ;
219:                         runningGameState.numberOfRoundsPlayed < game.params.numberOfRounds;
220:                         ++runningGameState.numberOfRoundsPlayed
221:                     ) {
222:                         if (
223:                             _stopGainOrStopLossHit(
224:                                 game.params.stopGain,
225:                                 game.params.stopLoss,
226:                                 runningGameState.netAmount
227:                             )
228:                         ) {
229:                             break;
230:                         }
231: 
232:                         bool isGold = runningGameState.randomWord % 2 != 0;
233: 
234:                         results[runningGameState.numberOfRoundsPlayed] = isGold;
235: 
236:                         
237:                         
238:                         if (game.isGold == isGold) {
239:                             uint256 protocolFee = (game.params.playAmountPerRound *
240:                                 2 *
241:                                 feeSplit.protocolFeeBasisPoints) / 10_000;
242:                             uint256 liquidityPoolFee = (game.params.playAmountPerRound *
243:                                 2 *
244:                                 feeSplit.liquidityPoolFeeBasisPoints) / 10_000;
245:                             runningGameState.netAmount += int256(
246:                                 game.params.playAmountPerRound *
247:                                     2 -
248:                                     protocolFee -
249:                                     liquidityPoolFee -
250:                                     game.params.playAmountPerRound
251:                             );
252:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
253:                                 game.params.playAmountPerRound *
254:                                 2 -
255:                                 protocolFee -
256:                                 liquidityPoolFee;
257:                             runningGameState.payout += runningGameState.payouts[runningGameState.numberOfRoundsPlayed];
258:                             fee.protocolFee += protocolFee;
259:                             fee.liquidityPoolFee += liquidityPoolFee;
260:                         } else {
261:                             runningGameState.netAmount -= int256(game.params.playAmountPerRound);
262:                         }
263: 
264:                         runningGameState.randomWord = uint256(keccak256(abi.encode(runningGameState.randomWord)));
265:                     }
266: 
267:                     _handlePayout(
268:                         player,
269:                         game.params,
270:                         runningGameState.numberOfRoundsPlayed,
271:                         runningGameState.payout,
272:                         fee.protocolFee
273:                     );
274:                     _transferVrfFee(game.params.vrfFee);
275: 
276:                     emit Flipper__GameCompleted(
277:                         game.params.blockNumber,
278:                         player,
279:                         results,
280:                         runningGameState.payouts,
281:                         runningGameState.numberOfRoundsPlayed,
282:                         fee.protocolFee,
283:                         fee.liquidityPoolFee
284:                     );
285: 
286:                     _deleteGame(player);
287:                 }
288:             }
289:         }
290:     }

['294']

294:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
295:         address player = randomnessRequests[requestId];
296: 
297:         if (player != address(0)) {
298:             LaserBlast__Game storage game = games[player];
299:             if (_hasLiquidityPool(game.params.currency)) {
300:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) {
301:                     randomnessRequests[requestId] = address(0);
302: 
303:                     RunningGameState memory runningGameState;
304:                     runningGameState.payouts = new uint256[](game.params.numberOfRounds); // <= FOUND
305:                     runningGameState.randomWord = randomWords[0];
306:                     uint256[] memory results = new uint256[](game.params.numberOfRounds); // <= FOUND
307: 
308:                     IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
309:                         address(this)
310:                     );
311: 
312:                     uint256 adjustedLiquidityPoolFeeBasisPoints = _applyMultiplierToLiquidityPoolFeeBasisPoints(
313:                         feeSplit.liquidityPoolFeeBasisPoints,
314:                         game.riskLevel
315:                     );
316: 
317:                     Fee memory fee;
318:                     uint256 playAmountPerRound = game.params.playAmountPerRound;
319: 
320:                     for (
321:                         ;
322:                         runningGameState.numberOfRoundsPlayed < game.params.numberOfRounds;
323:                         ++runningGameState.numberOfRoundsPlayed
324:                     ) {
325:                         if (
326:                             _stopGainOrStopLossHit(
327:                                 game.params.stopGain,
328:                                 game.params.stopLoss,
329:                                 runningGameState.netAmount
330:                             )
331:                         ) {
332:                             break;
333:                         }
334: 
335:                         (uint256 result, uint256 multiplier, uint256 randomWordForNextRound) = _dropTheBall(
336:                             game.riskLevel,
337:                             game.rowCount,
338:                             runningGameState.randomWord
339:                         );
340: 
341:                         uint256 protocolFee = (playAmountPerRound * multiplier * feeSplit.protocolFeeBasisPoints) / 1e8;
342:                         uint256 liquidityPoolFee = (playAmountPerRound *
343:                             multiplier *
344:                             adjustedLiquidityPoolFeeBasisPoints) / 1e8;
345: 
346:                         runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
347:                             (playAmountPerRound * multiplier) /
348:                             10_000 -
349:                             protocolFee -
350:                             liquidityPoolFee;
351:                         runningGameState.payout += runningGameState.payouts[runningGameState.numberOfRoundsPlayed];
352:                         runningGameState.netAmount += (int256(
353:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed]
354:                         ) - int256(playAmountPerRound));
355:                         fee.protocolFee += protocolFee;
356:                         fee.liquidityPoolFee += liquidityPoolFee;
357:                         results[runningGameState.numberOfRoundsPlayed] = result;
358: 
359:                         runningGameState.randomWord = randomWordForNextRound;
360:                     }
361: 
362:                     _handlePayout(
363:                         player,
364:                         game.params,
365:                         runningGameState.numberOfRoundsPlayed,
366:                         runningGameState.payout,
367:                         fee.protocolFee
368:                     );
369:                     _transferVrfFee(game.params.vrfFee);
370: 
371:                     emit LaserBlast__GameCompleted(
372:                         game.params.blockNumber,
373:                         player,
374:                         results,
375:                         runningGameState.payouts,
376:                         runningGameState.numberOfRoundsPlayed,
377:                         fee.protocolFee,
378:                         fee.liquidityPoolFee
379:                     );
380: 
381:                     _deleteGame(player);
382:                 }
383:             }
384:         }
385:     }

['238']

238:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
239:         address player = randomnessRequests[requestId];
240:         if (player != address(0)) {
241:             Quantum__Game storage game = games[player];
242:             if (_hasLiquidityPool(game.params.currency)) {
243:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) {
244:                     randomnessRequests[requestId] = address(0);
245: 
246:                     RunningGameState memory runningGameState;
247:                     runningGameState.payouts = new uint256[](game.params.numberOfRounds); // <= FOUND
248:                     runningGameState.randomWord = randomWords[0];
249:                     uint256[] memory results = new uint256[](game.params.numberOfRounds); // <= FOUND
250: 
251:                     IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
252:                         address(this)
253:                     );
254:                     Fee memory fee;
255: 
256:                     for (
257:                         ;
258:                         runningGameState.numberOfRoundsPlayed < game.params.numberOfRounds;
259:                         ++runningGameState.numberOfRoundsPlayed
260:                     ) {
261:                         if (
262:                             _stopGainOrStopLossHit(
263:                                 game.params.stopGain,
264:                                 game.params.stopLoss,
265:                                 runningGameState.netAmount
266:                             )
267:                         ) {
268:                             break;
269:                         }
270: 
271:                         results[runningGameState.numberOfRoundsPlayed] = runningGameState.randomWord % TOTAL_OUTCOMES;
272:                         if (
273:                             (game.isAbove &&
274:                                 results[runningGameState.numberOfRoundsPlayed] >=
275:                                 defineBoundary(calculateWinProbability(game.multiplier))) ||
276:                             (!game.isAbove &&
277:                                 results[runningGameState.numberOfRoundsPlayed] <
278:                                 calculateWinProbability(game.multiplier))
279:                         ) {
280:                             uint256 protocolFee = (game.multiplier *
281:                                 game.params.playAmountPerRound *
282:                                 feeSplit.protocolFeeBasisPoints) / 1e8;
283:                             uint256 liquidityPoolFee = (game.multiplier *
284:                                 game.params.playAmountPerRound *
285:                                 feeSplit.liquidityPoolFeeBasisPoints) / 1e8;
286: 
287:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
288:                                 ((game.multiplier * game.params.playAmountPerRound) / 10_000) -
289:                                 protocolFee -
290:                                 liquidityPoolFee;
291:                             runningGameState.payout += runningGameState.payouts[runningGameState.numberOfRoundsPlayed];
292:                             runningGameState.netAmount += int256(
293:                                 runningGameState.payouts[runningGameState.numberOfRoundsPlayed] -
294:                                     game.params.playAmountPerRound
295:                             );
296:                             fee.protocolFee += protocolFee;
297:                             fee.liquidityPoolFee += liquidityPoolFee;
298:                         } else {
299:                             runningGameState.netAmount -= int256(game.params.playAmountPerRound);
300:                         }
301:                         runningGameState.randomWord = uint256(keccak256(abi.encode(runningGameState.randomWord)));
302:                     }
303: 
304:                     _handlePayout(
305:                         player,
306:                         game.params,
307:                         runningGameState.numberOfRoundsPlayed,
308:                         runningGameState.payout,
309:                         fee.protocolFee
310:                     );
311:                     _transferVrfFee(game.params.vrfFee);
312: 
313:                     emit Quantum__GameCompleted(
314:                         game.params.blockNumber,
315:                         player,
316:                         results,
317:                         runningGameState.payouts,
318:                         runningGameState.numberOfRoundsPlayed,
319:                         fee.protocolFee,
320:                         fee.liquidityPoolFee
321:                     );
322: 
323:                     _deleteGame(player);
324:                 }
325:             }
326:         }
327:     }

[NonCritical-16] Constants should be on the left side of the comparison

Resolution

Putting constants on the left side of a comparison operator like == or < is a best practice known as "Yoda conditions", which can help prevent accidental assignment instead of comparison. In some programming languages, if a variable is mistakenly put on the left with a single = instead of ==, it assigns the constant's value to the variable without any compiler error. However, doing this with the constant on the left would generate an error, as constants cannot be assigned values. Although Solidity's static typing system prevents accidental assignments within conditionals, adopting this practice enhances code readability and consistency, especially when developers are working across multiple languages that support this convention.

Num of instances: 40

Findings

Click to show findings

['245']

245:         if (game.params.randomnessRequestedAt != 0)  // <= FOUND

['249']

249:         if (game.params.numberOfRounds == 0)  // <= FOUND

['255']

255:         if (grid & selectedTiles != 0)  // <= FOUND

['297']

297:         if (multiplier == 0)  // <= FOUND

['341']

341:         if (playAmountPerRound == 0)  // <= FOUND

['427']

427:        if (lavasCount == 0 || lavasCount >= GRID_SIZE)  // <= FOUND

['449']

449:         if (winProbability == 0)  // <= FOUND

['552']

552:        if (selectedTiles == 0)  // <= FOUND

['576']

576:        if (winProbabilities[lavasCount][selectedTilesCount] == 0)  // <= FOUND

['115']

115:         if (claimableAmount != 0)  // <= FOUND

['286']

286:        if (params.numberOfRounds == 0)  // <= FOUND

['331']

331:        if (numberOfRounds == 0)  // <= FOUND

['341']

341:         if (playAmountPerRound == 0)  // <= FOUND

['175']

175:         if (IERC4626(liquidityPool).totalSupply() == 0)  // <= FOUND

['197']

197:         if (requestedAt == 0)  // <= FOUND

['463']

463:        if (multipliers[_multiplierKey(riskLevel, rowCount, 0)] == 0)  // <= FOUND

['296']

296:         if (deposits[msg.sender].amount != 0)  // <= FOUND

['296']

296:         if (deposits[msg.sender].amount != 0)  // <= FOUND

['376']

376:         if (amount == 0)  // <= FOUND

['425']

425:         if (redemptions[msg.sender].shares != 0)  // <= FOUND

['376']

376:         if (amount == 0)  // <= FOUND

['376']

376:        if (amount == 0)  // <= FOUND

['135']

135:             if (multipliers[key] != 0)  // <= FOUND

['489']

489:        if (riskLevel == 1)  // <= FOUND

['258']

258:         if (payout > 0)  // <= FOUND

['261']

261:         if (protocolFee > 0)  // <= FOUND

['270']

270:        if (vrfFee > 0)  // <= FOUND

['303']

303:             if (params.vrfFee > 0)  // <= FOUND

['317']

317:        if (stopGain < 0 || stopLoss > 0)  // <= FOUND

['349']

349:        if (numberOfRounds > 0)  // <= FOUND

['571']

571:        if (_finalizationIncentive > 0.01 ether)  // <= FOUND

['230']

230:         if (_elapsedTimeRequiredForRefund > 1 days)  // <= FOUND

['251']

251:        if (basisPoints > 10_000)  // <= FOUND

['575']

575:         if (_timelockDelay < 5 seconds || _timelockDelay > 1 minutes)  // <= FOUND

['372']

372:        if (multiplier < 10_526 || multiplier > 10_000_000)  // <= FOUND

['298']

298:        if (_protocolFeeBasisPoints + _liquidityPoolFeeBasisPoints > 500)  // <= FOUND

['579']

579:         if (_finalizationForAllDelay > 5 minutes)  // <= FOUND

['226']

226:        if (_elapsedTimeRequiredForRefund < 10 minutes)  // <= FOUND

['358']

358:         if (_vrfParameters.callbackGasLimit < 100_000)  // <= FOUND

['354']

354:        if (_vrfParameters.minimumRequestConfirmations < 3)  // <= FOUND

[NonCritical-17] Overly complicated arithmetic

Resolution

To maintain readability in code, particularly in Solidity which can involve complex mathematical operations, it is often recommended to limit the number of arithmetic operations to a maximum of 2-3 per line. Too many operations in a single line can make the code difficult to read and understand, increase the likelihood of mistakes, and complicate the process of debugging and reviewing the code. Consider splitting such operations over more than one line, take special care when dealing with division however. Try to limit the number of arithmetic operations to a maximum of 3 per line.

Num of instances: 3

Findings

Click to show findings

['433']

433:         return
434:             ((1e18 - winProbability) * KELLY_FRACTION_SCALER) /
435:             ((multiplier * _liquidityProviderAdjustedReturn()) / 10_000 - 1e4) - // <= FOUND
436:             (winProbability * KELLY_FRACTION_SCALER) /
437:             1e4;

['191']

191:         return (5_000 * KELLY_FRACTION_SCALER) / multiplier - (5_000 * KELLY_FRACTION_SCALER) / 10_000; // <= FOUND

['227']

227:         return
228:             ((TOTAL_OUTCOMES - winProbability) * KELLY_FRACTION_SCALER) /
229:             ((multiplier * _liquidityProviderAdjustedReturn() * 1_000) / 10_000 - TOTAL_OUTCOMES) - // <= FOUND
230:             (winProbability * KELLY_FRACTION_SCALER) /
231:             TOTAL_OUTCOMES;

[NonCritical-18] Use of non-named numeric constants

Resolution

Magic numbers should be avoided in Solidity code to enhance readability, maintainability, and reduce the likelihood of errors. Magic numbers are hard-coded values with no clear meaning or context, which can create confusion and make the code harder to understand for developers. Using well-defined constants or variables with descriptive names instead of magic numbers not only clarifies the purpose and significance of the value but also simplifies code updates and modifications.

Num of instances: 44

Findings

Click to show findings

['312']

312:         uint256 payout = ((game.params.playAmountPerRound * multiplier) / 10_000) - // <= FOUND
313:             fee.protocolFee -
314:             fee.liquidityPoolFee;

['414']

414:         minPlayAmount = maxPlayAmountPerGame(currency, lavasCount, selectedTilesCount) / 10_000; // <= FOUND

['475']

475:                         if (
476:                             runningGameState.randomWord % 10_000 < // <= FOUND
477:                             (game.lavasCount * 10_000) / (GRID_SIZE - revealedTilesCount) // <= FOUND
478:                         ) {

['508']

508:                             runningGameState.payout =
509:                                 (game.params.playAmountPerRound * multiplier) /
510:                                 10_000 - // <= FOUND
511:                                 fee.protocolFee -
512:                                 fee.liquidityPoolFee;

['168']

168:         maxPlayAmount =
169:             (_liquidityPoolBalance(currency) *
170:                 kellyFraction() *
171:                 GAME_CONFIGURATION_MANAGER.kellyFractionBasisPoints(address(this))) /
172:             KELLY_FRACTION_SCALER /
173:             10_000; // <= FOUND

['189']

189:         uint256 multiplier = (_liquidityProviderAdjustedReturn() * 10_000) / 5_000 - 10_000; // <= FOUND

['191']

191:         return (5_000 * KELLY_FRACTION_SCALER) / multiplier - (5_000 * KELLY_FRACTION_SCALER) / 10_000; // <= FOUND

['239']

239:                             uint256 protocolFee = (game.params.playAmountPerRound *
240:                                 2 * // <= FOUND
241:                                 feeSplit.protocolFeeBasisPoints) / 10_000; // <= FOUND

['242']

242:                             uint256 liquidityPoolFee = (game.params.playAmountPerRound *
243:                                 2 * // <= FOUND
244:                                 feeSplit.liquidityPoolFeeBasisPoints) / 10_000; // <= FOUND

['394']

394:         minPlayAmountPerGame = maxPlayAmountPerGame / 10_000; // <= FOUND

['399']

399:         return 10_000 - feeSplit.liquidityPoolFeeBasisPoints; // <= FOUND

['226']

226:         if (_elapsedTimeRequiredForRefund < 10 minutes) { // <= FOUND

['251']

251:         if (basisPoints > 10_000) { // <= FOUND

['358']

358:         if (_vrfParameters.callbackGasLimit < 100_000) { // <= FOUND

['105']

105:         kellyFractions[1] = [6320, 9480, 11300, 9920, 10920, 10280, 9460, 8490, 7350]; // <= FOUND

['106']

106:         kellyFractions[2] = [4049, 2689, 1563, 1599, 1247, 869, 547, 634, 369]; // <= FOUND

['346']

346:                         runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
347:                             (playAmountPerRound * multiplier) /
348:                             10_000 - // <= FOUND
349:                             protocolFee -
350:                             liquidityPoolFee;

['291']

291:         uint256 depositFee = (amount * DEPOSIT_FEE_BASIS_POINTS) / 10_000; // <= FOUND

['179']

179:         maxPlayAmount =
180:             (_liquidityPoolBalance(currency) *
181:                 kellyFraction(multiplier) *
182:                 GAME_CONFIGURATION_MANAGER.kellyFractionBasisPoints(address(this))) /
183:             KELLY_FRACTION_SCALER /
184:             10_000; // <= FOUND

['227']

227:         return
228:             ((TOTAL_OUTCOMES - winProbability) * KELLY_FRACTION_SCALER) /
229:             ((multiplier * _liquidityProviderAdjustedReturn() * 1_000) / 10_000 - TOTAL_OUTCOMES) - // <= FOUND
230:             (winProbability * KELLY_FRACTION_SCALER) /
231:             TOTAL_OUTCOMES;

['287']

287:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
288:                                 ((game.multiplier * game.params.playAmountPerRound) / 10_000) - // <= FOUND
289:                                 protocolFee -
290:                                 liquidityPoolFee;

['372']

372:         if (multiplier < 10_526 || multiplier > 10_000_000) { // <= FOUND

['597']

597:             if ((tiles >> i) % 2 != 0) { // <= FOUND

['72']

72:         _transferETHAndWrapIfFailWithGasLimit(weth, receiver, amount, 2_300); // <= FOUND

['232']

232:                         bool isGold = runningGameState.randomWord % 2 != 0; // <= FOUND

['245']

245:                             runningGameState.netAmount += int256(
246:                                 game.params.playAmountPerRound *
247:                                     2 - // <= FOUND
248:                                     protocolFee -
249:                                     liquidityPoolFee -
250:                                     game.params.playAmountPerRound
251:                             );

['252']

252:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
253:                                 game.params.playAmountPerRound *
254:                                 2 - // <= FOUND
255:                                 protocolFee -
256:                                 liquidityPoolFee;

['144']

144:         VrfParameters memory _vrfParameters = VrfParameters({
145:             coordinator: _vrfCoordinator,
146:             subscriptionId: _subscriptionId,
147:             callbackGasLimit: 2_500_000, // <= FOUND
148:             minimumRequestConfirmations: 3, // <= FOUND
149:             vrfFee: 0.0003 ether,
150:             keyHash: _keyHash
151:         });

['104']

104:         kellyFractions[0] = [41300, 25900, 31400, 29500, 63700, 49000, 49700, 47300, 28800]; // <= FOUND

['403']

403:             if (randomWord % 2 == 0) { // <= FOUND

['413']

413:         uint256 bin = uint256(movement + int256(rowCount)) / 2; // <= FOUND

['491']

491:         } else if (riskLevel == 2) { // <= FOUND

['492']

492:             adjustedLiquidityPoolFeeBasisPoints = (liquidityPoolFeeBasisPoints * 3) / 2; // <= FOUND

['494']

494:             adjustedLiquidityPoolFeeBasisPoints = liquidityPoolFeeBasisPoints * 2; // <= FOUND

['393']

393:         _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, incentive, 2_300); // <= FOUND

['539']

539:             _transferETHAndWrapIfFailWithGasLimit(WETH, redeemer, assetsRedeemed, 2_300); // <= FOUND

['354']

354:         if (_vrfParameters.minimumRequestConfirmations < 3) { // <= FOUND

['493']

493:         } else if (riskLevel == 3) { // <= FOUND

['374']

374:         executable =
375:             randomnessRequestedAt - 5 minutes + GAME_CONFIGURATION_MANAGER.elapsedTimeRequiredForRefund() > // <= FOUND
376:             block.timestamp;

['298']

298:         if (_protocolFeeBasisPoints + _liquidityPoolFeeBasisPoints > 500) { // <= FOUND

['215']

215:         _setFinalizationParams(10 seconds, 5 minutes, 0.0003 ether); // <= FOUND

['575']

575:         if (_timelockDelay < 5 seconds || _timelockDelay > 1 minutes) { // <= FOUND

['579']

579:         if (_finalizationForAllDelay > 5 minutes) { // <= FOUND

['121']

121:         return 6; // <= FOUND

[NonCritical-19] Employ Explicit Casting to Bytes or Bytes32 for Enhanced Code Clarity and Meaning

Resolution

Smart contracts are complex entities, and clarity in their operations is fundamental to ensure that they function as intended. Casting a single argument instead of utilizing 'abi.encodePacked()' improves the transparency of the operation. It elucidates the intent of the code, reducing ambiguity and making it easier for auditors and developers to understand the code’s purpose. Such practices promote readability and maintainability, thus reducing the likelihood of errors and misunderstandings. Therefore, it's recommended to employ explicit casts for single arguments where possible, to increase the contract's comprehensibility and ensure a smoother review process.

Num of instances: 2

Findings

Click to show findings

['164']

164:     function initiateGameLiquidityPoolConnectionRequest(
165:         address game,
166:         address currency,
167:         address liquidityPool
168:     ) external onlyOwner {
169:         address liquidityPoolCurrency = IERC4626(liquidityPool).asset();
170: 
171:         if ((currency != liquidityPoolCurrency) && !(currency == address(0) && liquidityPoolCurrency == WETH)) {
172:             revert GameConfigurationManager__GameLiquidityPoolCurrencyMismatch();
173:         }
174: 
175:         if (IERC4626(liquidityPool).totalSupply() == 0) {
176:             gameLiquidityPool[game][currency] = liquidityPool;
177:             kellyFractionBasisPoints[game] = 10_000;
178:             emit GameAndLiquidityPoolConnected(game, currency, liquidityPool);
179:         } else {
180:             bytes32 requestId = keccak256(abi.encodePacked(game, currency, liquidityPool)); // <= FOUND
181:             gameLiquidityPoolConnectionRequests[requestId] = block.timestamp;
182:             emit GameAndLiquidityPoolConnectionRequestInitiated(game, currency, liquidityPool);
183:         }
184:     }

['189']

189:     function confirmGameLiquidityPoolConnectionRequest(
190:         address game,
191:         address currency,
192:         address liquidityPool
193:     ) external onlyOwner {
194:         bytes32 requestId = keccak256(abi.encodePacked(game, currency, liquidityPool)); // <= FOUND
195:         uint256 requestedAt = gameLiquidityPoolConnectionRequests[requestId];
196: 
197:         if (requestedAt == 0) {
198:             revert GameConfigurationManager__NoGameLiquidityPoolConnectionRequest();
199:         }
200: 
201:         if (block.timestamp - requestedAt < GAME_LIQUIDITY_POOL_CONNECTION_TIMELOCK) {
202:             revert GameConfigurationManager__GameLiquidityPoolConnectionRequestConfirmationIsTooEarly();
203:         }
204: 
205:         gameLiquidityPool[game][currency] = liquidityPool;
206:         kellyFractionBasisPoints[game] = 10_000;
207: 
208:         emit GameAndLiquidityPoolConnected(game, currency, liquidityPool);
209:     }

[NonCritical-20] Cyclomatic complexity in functions

Resolution

Cyclomatic complexity is a software metric used to measure the complexity of a program. It quantifies the number of linearly independent paths through a program's source code, giving an idea of how complex the control flow is. High cyclomatic complexity may indicate a higher risk of defects and can make the code harder to understand, test, and maintain. It often suggests that a function or method is trying to do too much, and a refactor might be needed. By breaking down complex functions into smaller, more focused pieces, you can improve readability, ease of testing, and overall maintainability.

Num of instances: 15

Findings

Click to show findings

['242']

242:     function playOngoing(uint32 selectedTiles, bool cashoutIfWon) external payable nonReentrant { // <= FOUND
243:         DontFallIn__Game storage game = games[msg.sender];
244: 
245:         if (game.params.randomnessRequestedAt != 0) {
246:             revert Game__OngoingRound();
247:         }
248: 
249:         if (game.params.numberOfRounds == 0) {
250:             revert Game__NoOngoingRound();
251:         }
252: 
253:         uint256 grid = game.grid;
254: 
255:         if (grid & selectedTiles != 0) {
256:             revert DontFallIn__TilesAlreadyRevealed();
257:         }
258: 
259:         _validateSelectedTilesMaxSize(selectedTiles);
260:         (uint256 selectedTilesCount, ) = _nonZeroTilesCount(selectedTiles);
261:         _validateNonZeroSelectedTiles(selectedTilesCount);
262: 
263:         (uint256 revealedTilesCount, ) = _nonZeroTilesCount(grid);
264:         _validateNonZeroMultiplier(game.lavasCount, selectedTilesCount + revealedTilesCount);
265: 
266:         
267:         
268:         
269:         _getGameLiquidityPool(game.params.currency);
270: 
271:         (, , , , uint240 vrfFee, ) = GAME_CONFIGURATION_MANAGER.vrfParameters();
272:         if (msg.value != vrfFee) {
273:             revert Game__InexactNativeTokensSupplied();
274:         }
275: 
276:         games[msg.sender].selectedTiles = selectedTiles;
277:         games[msg.sender].cashoutIfWon = cashoutIfWon;
278:         games[msg.sender].params.randomnessRequestedAt = uint40(block.timestamp);
279:         games[msg.sender].params.vrfFee = vrfFee;
280: 
281:         _requestRandomness();
282: 
283:         emit DontFallIn__GameContinued(game.params.blockNumber, msg.sender, selectedTiles, cashoutIfWon);
284:     }

['333']

333:     function cashoutOriginalAmount() external nonReentrant { // <= FOUND
334:         DontFallIn__Game storage game = games[msg.sender];
335: 
336:         if (game.params.randomnessRequestedAt != 0) {
337:             revert Game__OngoingRound();
338:         }
339: 
340:         uint256 playAmountPerRound = game.params.playAmountPerRound;
341:         if (playAmountPerRound == 0) {
342:             revert Game__ZeroPlayAmountPerRound();
343:         }
344: 
345:         address currency = game.params.currency;
346: 
347:         if (GAME_CONFIGURATION_MANAGER.getGameLiquidityPool(address(this), currency) != address(0)) {
348:             revert Game__LiquidityPoolConnected();
349:         }
350: 
351:         emit DontFallIn__GameCashedOutOriginalAmount(game.params.blockNumber, msg.sender, playAmountPerRound);
352: 
353:         _deleteGame(msg.sender);
354: 
355:         if (currency == address(0)) {
356:             _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, playAmountPerRound, gasleft());
357:         } else {
358:             _executeERC20DirectTransfer(currency, msg.sender, playAmountPerRound);
359:         }
360:     }

['459']

459:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
460:         address player = randomnessRequests[requestId];
461:         if (player != address(0)) {
462:             DontFallIn__Game storage game = games[player];
463:             if (_hasLiquidityPool(game.params.currency)) {
464:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) {
465:                     randomnessRequests[requestId] = address(0);
466: 
467:                     RunningGameState memory runningGameState;
468:                     runningGameState.randomWord = randomWords[0];
469: 
470:                     uint256 lavas;
471:                     (uint256 selectedTilesCount, uint256[] memory indices) = _nonZeroTilesCount(game.selectedTiles);
472:                     uint32 grid = game.grid;
473:                     (uint256 revealedTilesCount, ) = _nonZeroTilesCount(grid);
474:                     for (uint256 i; i < selectedTilesCount; ++i) {
475:                         if (
476:                             runningGameState.randomWord % 10_000 <
477:                             (game.lavasCount * 10_000) / (GRID_SIZE - revealedTilesCount)
478:                         ) {
479:                             lavas |= 1 << indices[i];
480:                         } else {
481:                             grid |= uint32(1 << indices[i]);
482:                         }
483: 
484:                         unchecked {
485:                             ++revealedTilesCount;
486:                         }
487: 
488:                         runningGameState.randomWord = uint256(keccak256(abi.encode(runningGameState.randomWord)));
489:                     }
490: 
491:                     _transferVrfFee(game.params.vrfFee);
492: 
493:                     if (lavas == 0) {
494:                         IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
495:                             address(this)
496:                         );
497: 
498:                         uint256 multiplier = _getMultiplier(winProbabilities[game.lavasCount][revealedTilesCount]);
499:                         if (game.cashoutIfWon) {
500:                             Fee memory fee = Fee({
501:                                 protocolFee: (game.params.playAmountPerRound *
502:                                     multiplier *
503:                                     feeSplit.protocolFeeBasisPoints) / 1e8,
504:                                 liquidityPoolFee: (game.params.playAmountPerRound *
505:                                     multiplier *
506:                                     feeSplit.liquidityPoolFeeBasisPoints) / 1e8
507:                             });
508:                             runningGameState.payout =
509:                                 (game.params.playAmountPerRound * multiplier) /
510:                                 10_000 -
511:                                 fee.protocolFee -
512:                                 fee.liquidityPoolFee;
513:                             _handlePayout(player, game.params, 1, runningGameState.payout, fee.protocolFee);
514:                             emit DontFallIn__GameWon(
515:                                 game.params.blockNumber,
516:                                 player,
517:                                 game.selectedTiles,
518:                                 runningGameState.payout,
519:                                 multiplier,
520:                                 fee.protocolFee,
521:                                 fee.liquidityPoolFee
522:                             );
523:                             _deleteGame(player);
524:                         } else {
525:                             game.params.randomnessRequestedAt = 0;
526:                             game.params.vrfFee = 0;
527:                             game.grid = grid;
528:                             game.multiplier = uint176(multiplier);
529: 
530:                             emit DontFallIn__GamePlayed(
531:                                 game.params.blockNumber,
532:                                 player,
533:                                 game.selectedTiles,
534:                                 game.multiplier
535:                             );
536:                         }
537:                     } else {
538:                         _handlePayout(player, game.params, 1, 0, 0);
539:                         emit DontFallIn__GameLost(game.params.blockNumber, player, game.selectedTiles, lavas);
540:                         _deleteGame(player);
541:                     }
542:                 }
543:             }
544:         }
545:     }

['198']

198:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
199:         address player = randomnessRequests[requestId];
200: 
201:         if (player != address(0)) {
202:             Flipper__Game storage game = games[player];
203:             if (_hasLiquidityPool(game.params.currency)) {
204:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) {
205:                     randomnessRequests[requestId] = address(0);
206: 
207:                     RunningGameState memory runningGameState;
208:                     runningGameState.randomWord = randomWords[0];
209:                     runningGameState.payouts = new uint256[](game.params.numberOfRounds);
210:                     bool[] memory results = new bool[](game.params.numberOfRounds);
211: 
212:                     IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
213:                         address(this)
214:                     );
215:                     Fee memory fee;
216: 
217:                     for (
218:                         ;
219:                         runningGameState.numberOfRoundsPlayed < game.params.numberOfRounds;
220:                         ++runningGameState.numberOfRoundsPlayed
221:                     ) {
222:                         if (
223:                             _stopGainOrStopLossHit(
224:                                 game.params.stopGain,
225:                                 game.params.stopLoss,
226:                                 runningGameState.netAmount
227:                             )
228:                         ) {
229:                             break;
230:                         }
231: 
232:                         bool isGold = runningGameState.randomWord % 2 != 0;
233: 
234:                         results[runningGameState.numberOfRoundsPlayed] = isGold;
235: 
236:                         
237:                         
238:                         if (game.isGold == isGold) {
239:                             uint256 protocolFee = (game.params.playAmountPerRound *
240:                                 2 *
241:                                 feeSplit.protocolFeeBasisPoints) / 10_000;
242:                             uint256 liquidityPoolFee = (game.params.playAmountPerRound *
243:                                 2 *
244:                                 feeSplit.liquidityPoolFeeBasisPoints) / 10_000;
245:                             runningGameState.netAmount += int256(
246:                                 game.params.playAmountPerRound *
247:                                     2 -
248:                                     protocolFee -
249:                                     liquidityPoolFee -
250:                                     game.params.playAmountPerRound
251:                             );
252:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
253:                                 game.params.playAmountPerRound *
254:                                 2 -
255:                                 protocolFee -
256:                                 liquidityPoolFee;
257:                             runningGameState.payout += runningGameState.payouts[runningGameState.numberOfRoundsPlayed];
258:                             fee.protocolFee += protocolFee;
259:                             fee.liquidityPoolFee += liquidityPoolFee;
260:                         } else {
261:                             runningGameState.netAmount -= int256(game.params.playAmountPerRound);
262:                         }
263: 
264:                         runningGameState.randomWord = uint256(keccak256(abi.encode(runningGameState.randomWord)));
265:                     }
266: 
267:                     _handlePayout(
268:                         player,
269:                         game.params,
270:                         runningGameState.numberOfRoundsPlayed,
271:                         runningGameState.payout,
272:                         fee.protocolFee
273:                     );
274:                     _transferVrfFee(game.params.vrfFee);
275: 
276:                     emit Flipper__GameCompleted(
277:                         game.params.blockNumber,
278:                         player,
279:                         results,
280:                         runningGameState.payouts,
281:                         runningGameState.numberOfRoundsPlayed,
282:                         fee.protocolFee,
283:                         fee.liquidityPoolFee
284:                     );
285: 
286:                     _deleteGame(player);
287:                 }
288:             }
289:         }
290:     }

['294']

294:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
295:         address player = randomnessRequests[requestId];
296: 
297:         if (player != address(0)) {
298:             LaserBlast__Game storage game = games[player];
299:             if (_hasLiquidityPool(game.params.currency)) {
300:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) {
301:                     randomnessRequests[requestId] = address(0);
302: 
303:                     RunningGameState memory runningGameState;
304:                     runningGameState.payouts = new uint256[](game.params.numberOfRounds);
305:                     runningGameState.randomWord = randomWords[0];
306:                     uint256[] memory results = new uint256[](game.params.numberOfRounds);
307: 
308:                     IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
309:                         address(this)
310:                     );
311: 
312:                     uint256 adjustedLiquidityPoolFeeBasisPoints = _applyMultiplierToLiquidityPoolFeeBasisPoints(
313:                         feeSplit.liquidityPoolFeeBasisPoints,
314:                         game.riskLevel
315:                     );
316: 
317:                     Fee memory fee;
318:                     uint256 playAmountPerRound = game.params.playAmountPerRound;
319: 
320:                     for (
321:                         ;
322:                         runningGameState.numberOfRoundsPlayed < game.params.numberOfRounds;
323:                         ++runningGameState.numberOfRoundsPlayed
324:                     ) {
325:                         if (
326:                             _stopGainOrStopLossHit(
327:                                 game.params.stopGain,
328:                                 game.params.stopLoss,
329:                                 runningGameState.netAmount
330:                             )
331:                         ) {
332:                             break;
333:                         }
334: 
335:                         (uint256 result, uint256 multiplier, uint256 randomWordForNextRound) = _dropTheBall(
336:                             game.riskLevel,
337:                             game.rowCount,
338:                             runningGameState.randomWord
339:                         );
340: 
341:                         uint256 protocolFee = (playAmountPerRound * multiplier * feeSplit.protocolFeeBasisPoints) / 1e8;
342:                         uint256 liquidityPoolFee = (playAmountPerRound *
343:                             multiplier *
344:                             adjustedLiquidityPoolFeeBasisPoints) / 1e8;
345: 
346:                         runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
347:                             (playAmountPerRound * multiplier) /
348:                             10_000 -
349:                             protocolFee -
350:                             liquidityPoolFee;
351:                         runningGameState.payout += runningGameState.payouts[runningGameState.numberOfRoundsPlayed];
352:                         runningGameState.netAmount += (int256(
353:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed]
354:                         ) - int256(playAmountPerRound));
355:                         fee.protocolFee += protocolFee;
356:                         fee.liquidityPoolFee += liquidityPoolFee;
357:                         results[runningGameState.numberOfRoundsPlayed] = result;
358: 
359:                         runningGameState.randomWord = randomWordForNextRound;
360:                     }
361: 
362:                     _handlePayout(
363:                         player,
364:                         game.params,
365:                         runningGameState.numberOfRoundsPlayed,
366:                         runningGameState.payout,
367:                         fee.protocolFee
368:                     );
369:                     _transferVrfFee(game.params.vrfFee);
370: 
371:                     emit LaserBlast__GameCompleted(
372:                         game.params.blockNumber,
373:                         player,
374:                         results,
375:                         runningGameState.payouts,
376:                         runningGameState.numberOfRoundsPlayed,
377:                         fee.protocolFee,
378:                         fee.liquidityPoolFee
379:                     );
380: 
381:                     _deleteGame(player);
382:                 }
383:             }
384:         }
385:     }

['238']

238:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant { // <= FOUND
239:         address player = randomnessRequests[requestId];
240:         if (player != address(0)) {
241:             Quantum__Game storage game = games[player];
242:             if (_hasLiquidityPool(game.params.currency)) {
243:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) {
244:                     randomnessRequests[requestId] = address(0);
245: 
246:                     RunningGameState memory runningGameState;
247:                     runningGameState.payouts = new uint256[](game.params.numberOfRounds);
248:                     runningGameState.randomWord = randomWords[0];
249:                     uint256[] memory results = new uint256[](game.params.numberOfRounds);
250: 
251:                     IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
252:                         address(this)
253:                     );
254:                     Fee memory fee;
255: 
256:                     for (
257:                         ;
258:                         runningGameState.numberOfRoundsPlayed < game.params.numberOfRounds;
259:                         ++runningGameState.numberOfRoundsPlayed
260:                     ) {
261:                         if (
262:                             _stopGainOrStopLossHit(
263:                                 game.params.stopGain,
264:                                 game.params.stopLoss,
265:                                 runningGameState.netAmount
266:                             )
267:                         ) {
268:                             break;
269:                         }
270: 
271:                         results[runningGameState.numberOfRoundsPlayed] = runningGameState.randomWord % TOTAL_OUTCOMES;
272:                         if (
273:                             (game.isAbove &&
274:                                 results[runningGameState.numberOfRoundsPlayed] >=
275:                                 defineBoundary(calculateWinProbability(game.multiplier))) ||
276:                             (!game.isAbove &&
277:                                 results[runningGameState.numberOfRoundsPlayed] <
278:                                 calculateWinProbability(game.multiplier))
279:                         ) {
280:                             uint256 protocolFee = (game.multiplier *
281:                                 game.params.playAmountPerRound *
282:                                 feeSplit.protocolFeeBasisPoints) / 1e8;
283:                             uint256 liquidityPoolFee = (game.multiplier *
284:                                 game.params.playAmountPerRound *
285:                                 feeSplit.liquidityPoolFeeBasisPoints) / 1e8;
286: 
287:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
288:                                 ((game.multiplier * game.params.playAmountPerRound) / 10_000) -
289:                                 protocolFee -
290:                                 liquidityPoolFee;
291:                             runningGameState.payout += runningGameState.payouts[runningGameState.numberOfRoundsPlayed];
292:                             runningGameState.netAmount += int256(
293:                                 runningGameState.payouts[runningGameState.numberOfRoundsPlayed] -
294:                                     game.params.playAmountPerRound
295:                             );
296:                             fee.protocolFee += protocolFee;
297:                             fee.liquidityPoolFee += liquidityPoolFee;
298:                         } else {
299:                             runningGameState.netAmount -= int256(game.params.playAmountPerRound);
300:                         }
301:                         runningGameState.randomWord = uint256(keccak256(abi.encode(runningGameState.randomWord)));
302:                     }
303: 
304:                     _handlePayout(
305:                         player,
306:                         game.params,
307:                         runningGameState.numberOfRoundsPlayed,
308:                         runningGameState.payout,
309:                         fee.protocolFee
310:                     );
311:                     _transferVrfFee(game.params.vrfFee);
312: 
313:                     emit Quantum__GameCompleted(
314:                         game.params.blockNumber,
315:                         player,
316:                         results,
317:                         runningGameState.payouts,
318:                         runningGameState.numberOfRoundsPlayed,
319:                         fee.protocolFee,
320:                         fee.liquidityPoolFee
321:                     );
322: 
323:                     _deleteGame(player);
324:                 }
325:             }
326:         }
327:     }

[]

196:     function _escrowPlayAmountAndChargeVrfFee(
197:         address currency,
198:         uint256 numberOfRounds,
199:         uint256 playAmountPerRound,
200:         uint256 vrfFee
201:     ) internal {
202:         if (currency == address(0)) {
203:             if (msg.value != playAmountPerRound * numberOfRounds + vrfFee) {
204:                 revert Game__InexactNativeTokensSupplied();
205:             }
206:         } else {
207:             if (msg.value != vrfFee) {
208:                 revert Game__InexactNativeTokensSupplied();
209:             }
210: 
211:             TRANSFER_MANAGER.transferERC20(currency, msg.sender, address(this), playAmountPerRound * numberOfRounds);
212:         }
213:     }

['285']

285:     function _refund(Game__GameParams storage params) internal { // <= FOUND
286:         if (params.numberOfRounds == 0) {
287:             revert Game__NoOngoingRound();
288:         }
289: 
290:         if (
291:             params.randomnessRequestedAt + GAME_CONFIGURATION_MANAGER.elapsedTimeRequiredForRefund() > block.timestamp
292:         ) {
293:             revert Game__TooEarlyForARefund();
294:         }
295: 
296:         address currency = params.currency;
297:         uint256 totalPlayAmount = params.playAmountPerRound * params.numberOfRounds;
298:         if (currency == address(0)) {
299:             totalPlayAmount += params.vrfFee;
300:             _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, totalPlayAmount, gasleft());
301:         } else {
302:             _executeERC20DirectTransfer(currency, msg.sender, totalPlayAmount);
303:             if (params.vrfFee > 0) {
304:                 _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, params.vrfFee, gasleft());
305:             }
306:         }
307: 
308:         emit Game__Refunded(params.blockNumber, msg.sender, totalPlayAmount);
309:     }

[]

327:     function _validateNumberOfRoundsAndPlayAmountPerRound(
328:         uint256 numberOfRounds,
329:         uint256 playAmountPerRound
330:     ) internal view {
331:         if (numberOfRounds == 0) {
332:             revert Game__ZeroNumberOfRounds();
333:         }
334: 
335:         if (numberOfRounds > GAME_CONFIGURATION_MANAGER.maximumNumberOfRounds()) {
336:             revert Game__TooManyRounds();
337:         }
338: 
339:         if (playAmountPerRound == 0) {
340:             revert Game__ZeroPlayAmountPerRound();
341:         }
342:     }

['353']

353:     function _setVrfParameters(VrfParameters memory _vrfParameters) private { // <= FOUND
354:         if (_vrfParameters.minimumRequestConfirmations < 3) {
355:             revert GameConfigurationManager__VrfMinimumRequestConfirmationsTooLow();
356:         }
357: 
358:         if (_vrfParameters.callbackGasLimit < 100_000) {
359:             revert GameConfigurationManager__VrfCallbackGasLimitTooLow();
360:         }
361: 
362:         if (vrfParameters.coordinator == address(0)) {
363:             vrfParameters.coordinator = _vrfParameters.coordinator;
364:         }
365:         vrfParameters.subscriptionId = _vrfParameters.subscriptionId;
366:         vrfParameters.callbackGasLimit = _vrfParameters.callbackGasLimit;
367:         vrfParameters.minimumRequestConfirmations = _vrfParameters.minimumRequestConfirmations;
368:         vrfParameters.vrfFee = _vrfParameters.vrfFee;
369:         vrfParameters.keyHash = _vrfParameters.keyHash;
370: 
371:         emit VrfParametersUpdated(
372:             vrfParameters.coordinator,
373:             _vrfParameters.subscriptionId,
374:             _vrfParameters.callbackGasLimit,
375:             _vrfParameters.minimumRequestConfirmations,
376:             _vrfParameters.vrfFee,
377:             _vrfParameters.keyHash
378:         );
379:     }

['117']

117:     function setMultipliers(uint128 riskLevel, uint128 rowCount, uint256[] memory _multipliers) external onlyOwner { // <= FOUND
118:         _validateRiskLevel(riskLevel);
119:         _validateRowCount(rowCount);
120: 
121:         uint256 binsCount = _multipliers.length;
122: 
123:         if (binsCount != rowCount + 1) {
124:             revert LaserBlast__BinsCountMustBeOneHigherThanRowCount();
125:         }
126: 
127:         for (uint256 i; i < binsCount; ++i) {
128:             uint256 multiplier = _multipliers[i];
129:             if (multiplier == 0) {
130:                 revert Game__ZeroMultiplier();
131:             }
132: 
133:             bytes32 key = _multiplierKey(riskLevel, rowCount, i);
134: 
135:             if (multipliers[key] != 0) {
136:                 revert LaserBlast__MultiplierAlreadySet();
137:             }
138: 
139:             multipliers[key] = multiplier;
140:         }
141: 
142:         emit LaserBlast__MultipliersSet(riskLevel, rowCount, _multipliers);
143:     }

[]

485:     function _applyMultiplierToLiquidityPoolFeeBasisPoints(
486:         uint256 liquidityPoolFeeBasisPoints,
487:         uint256 riskLevel
488:     ) private pure returns (uint256 adjustedLiquidityPoolFeeBasisPoints) {
489:         if (riskLevel == 1) {
490:             adjustedLiquidityPoolFeeBasisPoints = liquidityPoolFeeBasisPoints;
491:         } else if (riskLevel == 2) {
492:             adjustedLiquidityPoolFeeBasisPoints = (liquidityPoolFeeBasisPoints * 3) / 2;
493:         } else if (riskLevel == 3) {
494:             adjustedLiquidityPoolFeeBasisPoints = liquidityPoolFeeBasisPoints * 2;
495:         }
496:     }

['374']

374:     function finalizeDeposit(address depositor) external nonReentrant { // <= FOUND
375:         uint256 amount = deposits[depositor].amount;
376:         if (amount == 0) {
377:             revert LiquidityPoolRouter__NoOngoingDeposit();
378:         }
379: 
380:         uint256 initializedAt = deposits[depositor].initializedAt;
381:         _validateTimelockIsOver(initializedAt);
382:         _validateFinalizationIsOpenForAll(depositor, initializedAt);
383: 
384:         address payable liquidityPool = payable(deposits[depositor].liquidityPool);
385:         address token = ERC4626(liquidityPool).asset();
386:         uint256 expectedShares = deposits[depositor].expectedShares;
387:         uint256 actualShares = ERC4626(liquidityPool).previewDeposit(amount);
388:         uint256 incentive = deposits[depositor].finalizationIncentive;
389: 
390:         deposits[depositor] = Deposit(address(0), 0, 0, 0, 0);
391:         pendingDeposits[liquidityPool] -= amount;
392: 
393:         _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, incentive, 2_300);
394: 
395:         uint256 sharesMinted;
396:         uint256 amountRequired;
397:         if (expectedShares >= actualShares) {
398:             amountRequired = amount;
399:             sharesMinted = _deposit(token, liquidityPool, amountRequired, depositor);
400:         } else {
401:             amountRequired = ERC4626(liquidityPool).previewMint(expectedShares);
402:             sharesMinted = _deposit(token, liquidityPool, amountRequired, depositor);
403:             if (token == WETH) {
404:                 _transferETHAndWrapIfFailWithGasLimit(WETH, liquidityPool, amount - amountRequired, gasleft());
405:             } else {
406:                 _executeERC20DirectTransfer(token, liquidityPool, amount - amountRequired);
407:             }
408:         }
409: 
410:         emit LiquidityPoolRouter__DepositFinalized(msg.sender, depositor, liquidityPool, amountRequired, sharesMinted);
411:     }

[]

566:     function _setFinalizationParams(
567:         uint80 _timelockDelay,
568:         uint80 _finalizationForAllDelay,
569:         uint80 _finalizationIncentive
570:     ) private {
571:         if (_finalizationIncentive > 0.01 ether) {
572:             revert LiquidityPoolRouter__FinalizationIncentiveTooHigh();
573:         }
574: 
575:         if (_timelockDelay < 5 seconds || _timelockDelay > 1 minutes) {
576:             revert LiquidityPoolRouter__InvalidTimelockDelay();
577:         }
578: 
579:         if (_finalizationForAllDelay > 5 minutes) {
580:             revert LiquidityPoolRouter__FinalizationForAllDelayTooHigh();
581:         }
582: 
583:         finalizationParams = FinalizationParams(_timelockDelay, _finalizationForAllDelay, _finalizationIncentive);
584: 
585:         emit LiquidityPoolRouter__FinalizationParamsUpdated(
586:             _timelockDelay,
587:             _finalizationForAllDelay,
588:             _finalizationIncentive
589:         );
590:     }

['612']

612:     function _validateDepositAmount(address liquidityPool, uint256 amount) private view { // <= FOUND
613:         if (amount == 0) {
614:             revert LiquidityPoolRouter__DepositAmountTooLow();
615:         }
616: 
617:         if (amount < depositLimit[liquidityPool].minDepositAmount) {
618:             revert LiquidityPoolRouter__DepositAmountTooLow();
619:         }
620: 
621:         if (amount > depositLimit[liquidityPool].maxDepositAmount) {
622:             revert LiquidityPoolRouter__DepositAmountTooHigh();
623:         }
624: 
625:         if (
626:             IERC20(ERC4626(liquidityPool).asset()).balanceOf(liquidityPool) + amount + pendingDeposits[liquidityPool] >
627:             depositLimit[liquidityPool].maxBalance
628:         ) {
629:             revert LiquidityPoolRouter__DepositAmountTooHigh();
630:         }
631:     }

[NonCritical-21] Events may be emitted out of order due to code not follow the best practice of check-effects-interaction

Resolution

The "check-effects-interaction" pattern also impacts event ordering. When a contract doesn't adhere to this pattern, events might be emitted in a sequence that doesn't reflect the actual logical flow of operations. This can cause confusion during event tracking, potentially leading to erroneous off-chain interpretations. To rectify this, always ensure that checks are performed first, state modifications come next, and interactions with external contracts or addresses are done last. This will ensure events are emitted in a logical, consistent manner, providing a clear and accurate chronological record of on-chain actions for off-chain systems and observers.

Num of instances: 11

Findings

Click to show findings

['61']

61:     function transferPayoutToPlayer(
62:         address game,
63:         uint256 amount,
64:         address receiver
65:     ) external nonReentrant whenNotPaused {
66:         _onlyGameConfigurationManager();
67:         address currency = asset();
68:         uint256 balance = IERC20(currency).balanceOf(address(this)); // <= FOUND
69:         if (balance < amount) {
70:             emit InsufficientFundsForPayout(game, receiver, currency, amount - balance); // <= FOUND
71:             amount = balance;
72:         }
73:         _executeERC20DirectTransfer(currency, receiver, amount);
74:         emit PayoutTransferred(game, receiver, currency, amount); // <= FOUND
75:     }

['80']

80:     function transferProtocolFee(
81:         address game,
82:         uint256 amount,
83:         address protocolFeeRecipient
84:     ) external nonReentrant whenNotPaused {
85:         _onlyGameConfigurationManager();
86:         address currency = asset();
87:         uint256 balance = IERC20(currency).balanceOf(address(this)); // <= FOUND
88:         if (balance >= amount) {
89:             _executeERC20DirectTransfer(currency, protocolFeeRecipient, amount);
90:             emit ProtocolFeeTransferred(game, protocolFeeRecipient, currency, amount); // <= FOUND
91:         }
92:     }

['55']

55:     function transferPayoutToPlayer(
56:         address game,
57:         uint256 amount,
58:         address receiver
59:     ) external nonReentrant whenNotPaused {
60:         _onlyGameConfigurationManager();
61: 
62:         address weth = asset();
63: 
64:         uint256 balance = IERC20(weth).balanceOf(address(this)); // <= FOUND
65:         if (balance < amount) {
66:             emit InsufficientFundsForPayout(game, receiver, address(0), amount - balance); // <= FOUND
67:             amount = balance;
68:         }
69: 
70:         IWETH(weth).withdraw(amount); // <= FOUND
71: 
72:         _transferETHAndWrapIfFailWithGasLimit(weth, receiver, amount, 2_300);
73: 
74:         emit PayoutTransferred(game, receiver, address(0), amount); // <= FOUND
75:     }

['80']

80:     function transferProtocolFee(
81:         address game,
82:         uint256 amount,
83:         address protocolFeeRecipient
84:     ) external nonReentrant whenNotPaused {
85:         _onlyGameConfigurationManager();
86:         address weth = asset();
87:         uint256 balance = IERC20(weth).balanceOf(address(this)); // <= FOUND
88:         if (balance >= amount) {
89:             IWETH(weth).withdraw(amount); // <= FOUND
90:             _transferETHAndWrapIfFailWithGasLimit(weth, protocolFeeRecipient, amount, gasleft());
91:             emit ProtocolFeeTransferred(game, protocolFeeRecipient, address(0), amount); // <= FOUND
92:         }
93:     }

['164']

164:     function initiateGameLiquidityPoolConnectionRequest(
165:         address game,
166:         address currency,
167:         address liquidityPool
168:     ) external onlyOwner {
169:         address liquidityPoolCurrency = IERC4626(liquidityPool).asset(); // <= FOUND
170: 
171:         if ((currency != liquidityPoolCurrency) && !(currency == address(0) && liquidityPoolCurrency == WETH)) {
172:             revert GameConfigurationManager__GameLiquidityPoolCurrencyMismatch();
173:         }
174: 
175:         if (IERC4626(liquidityPool).totalSupply() == 0) { // <= FOUND
176:             gameLiquidityPool[game][currency] = liquidityPool;
177:             kellyFractionBasisPoints[game] = 10_000;
178:             emit GameAndLiquidityPoolConnected(game, currency, liquidityPool); // <= FOUND
179:         } else {
180:             bytes32 requestId = keccak256(abi.encodePacked(game, currency, liquidityPool));
181:             gameLiquidityPoolConnectionRequests[requestId] = block.timestamp;
182:             emit GameAndLiquidityPoolConnectionRequestInitiated(game, currency, liquidityPool); // <= FOUND
183:         }
184:     }

['228']

228:     function addLiquidityPool(address liquidityPool) external onlyOwner { // <= FOUND
229:         address token = ERC4626(liquidityPool).asset(); // <= FOUND
230:         if (liquidityPools[token] != address(0)) {
231:             revert LiquidityPoolRouter__TokenAlreadyHasLiquidityPool();
232:         }
233:         liquidityPools[token] = liquidityPool;
234:         emit LiquidityPoolRouter__LiquidityPoolAdded(token, liquidityPool); // <= FOUND
235:     }

['284']

284:     function depositETH(uint256 amount) external payable nonReentrant whenNotPaused { // <= FOUND
285:         address liquidityPool = _getLiquidityPoolOrRevert(WETH);
286: 
287:         if (amount + finalizationParams.finalizationIncentive != msg.value) {
288:             revert LiquidityPoolRouter__FinalizationIncentiveNotPaid();
289:         }
290: 
291:         uint256 depositFee = (amount * DEPOSIT_FEE_BASIS_POINTS) / 10_000;
292:         amount -= depositFee;
293: 
294:         _validateDepositAmount(liquidityPool, amount);
295: 
296:         if (deposits[msg.sender].amount != 0) {
297:             revert LiquidityPoolRouter__OngoingDeposit();
298:         }
299: 
300:         _transferETHAndWrapIfFailWithGasLimit(WETH, owner, depositFee, gasleft());
301: 
302:         uint256 expectedShares = ERC4626(liquidityPool).previewDeposit(amount); // <= FOUND
303: 
304:         deposits[msg.sender] = Deposit(
305:             liquidityPool,
306:             amount,
307:             expectedShares,
308:             block.timestamp,
309:             finalizationParams.finalizationIncentive
310:         );
311:         pendingDeposits[liquidityPool] += amount;
312: 
313:         emit LiquidityPoolRouter__DepositInitialized( // <= FOUND
314:             msg.sender,
315:             liquidityPool,
316:             amount + depositFee,
317:             expectedShares,
318:             finalizationParams.finalizationIncentive
319:         );
320:     }

['329']

329:     function deposit(address token, uint256 amount) external payable nonReentrant whenNotPaused { // <= FOUND
330:         address liquidityPool = _getLiquidityPoolOrRevert(token);
331: 
332:         _validateFinalizationIncentivePayment();
333: 
334:         uint256 depositFee = (amount * DEPOSIT_FEE_BASIS_POINTS) / 10_000;
335:         amount -= depositFee;
336: 
337:         _validateDepositAmount(liquidityPool, amount);
338: 
339:         if (deposits[msg.sender].amount != 0) {
340:             revert LiquidityPoolRouter__OngoingDeposit();
341:         }
342: 
343:         TRANSFER_MANAGER.transferERC20(token, msg.sender, address(this), amount);
344:         TRANSFER_MANAGER.transferERC20(token, msg.sender, owner, depositFee);
345: 
346:         uint256 expectedShares = ERC4626(liquidityPool).previewDeposit(amount); // <= FOUND
347: 
348:         deposits[msg.sender] = Deposit(
349:             liquidityPool,
350:             amount,
351:             expectedShares,
352:             block.timestamp,
353:             finalizationParams.finalizationIncentive
354:         );
355:         pendingDeposits[liquidityPool] += amount;
356: 
357:         emit LiquidityPoolRouter__DepositInitialized( // <= FOUND
358:             msg.sender,
359:             liquidityPool,
360:             amount + depositFee,
361:             expectedShares,
362:             finalizationParams.finalizationIncentive
363:         );
364:     }

['374']

374:     function finalizeDeposit(address depositor) external nonReentrant { // <= FOUND
375:         uint256 amount = deposits[depositor].amount;
376:         if (amount == 0) {
377:             revert LiquidityPoolRouter__NoOngoingDeposit();
378:         }
379: 
380:         uint256 initializedAt = deposits[depositor].initializedAt;
381:         _validateTimelockIsOver(initializedAt);
382:         _validateFinalizationIsOpenForAll(depositor, initializedAt);
383: 
384:         address payable liquidityPool = payable(deposits[depositor].liquidityPool);
385:         address token = ERC4626(liquidityPool).asset(); // <= FOUND
386:         uint256 expectedShares = deposits[depositor].expectedShares;
387:         uint256 actualShares = ERC4626(liquidityPool).previewDeposit(amount); // <= FOUND
388:         uint256 incentive = deposits[depositor].finalizationIncentive;
389: 
390:         deposits[depositor] = Deposit(address(0), 0, 0, 0, 0);
391:         pendingDeposits[liquidityPool] -= amount;
392: 
393:         _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, incentive, 2_300);
394: 
395:         uint256 sharesMinted;
396:         uint256 amountRequired;
397:         if (expectedShares >= actualShares) {
398:             amountRequired = amount;
399:             sharesMinted = _deposit(token, liquidityPool, amountRequired, depositor);
400:         } else {
401:             amountRequired = ERC4626(liquidityPool).previewMint(expectedShares); // <= FOUND
402:             sharesMinted = _deposit(token, liquidityPool, amountRequired, depositor);
403:             if (token == WETH) {
404:                 _transferETHAndWrapIfFailWithGasLimit(WETH, liquidityPool, amount - amountRequired, gasleft());
405:             } else {
406:                 _executeERC20DirectTransfer(token, liquidityPool, amount - amountRequired);
407:             }
408:         }
409: 
410:         emit LiquidityPoolRouter__DepositFinalized(msg.sender, depositor, liquidityPool, amountRequired, sharesMinted); // <= FOUND
411:     }

['420']

420:     function redeem(address token, uint256 amount) external payable nonReentrant { // <= FOUND
421:         address liquidityPool = _getLiquidityPoolOrRevert(token);
422: 
423:         _validateFinalizationIncentivePayment();
424: 
425:         if (redemptions[msg.sender].shares != 0) {
426:             revert LiquidityPoolRouter__OngoingRedemption();
427:         }
428: 
429:         TRANSFER_MANAGER.transferERC20(liquidityPool, msg.sender, address(this), amount);
430: 
431:         uint256 expectedAssets = ERC4626(liquidityPool).previewRedeem(amount); // <= FOUND
432: 
433:         redemptions[msg.sender] = Redemption(
434:             liquidityPool,
435:             amount,
436:             expectedAssets,
437:             block.timestamp,
438:             finalizationParams.finalizationIncentive
439:         );
440: 
441:         emit LiquidityPoolRouter__RedemptionInitialized( // <= FOUND
442:             msg.sender,
443:             liquidityPool,
444:             amount,
445:             expectedAssets,
446:             finalizationParams.finalizationIncentive
447:         );
448:     }

['458']

458:     function finalizeRedemption(address redeemer) external nonReentrant { // <= FOUND
459:         uint256 amount = redemptions[redeemer].shares;
460:         if (amount == 0) {
461:             revert LiquidityPoolRouter__NoOngoingRedemption();
462:         }
463: 
464:         uint256 initializedAt = redemptions[redeemer].initializedAt;
465:         _validateTimelockIsOver(initializedAt);
466:         _validateFinalizationIsOpenForAll(redeemer, initializedAt);
467: 
468:         address payable liquidityPool = payable(redemptions[redeemer].liquidityPool);
469:         address token = ERC4626(liquidityPool).asset(); // <= FOUND
470:         uint256 expectedAssets = redemptions[redeemer].expectedAssets;
471:         uint256 incentive = redemptions[redeemer].finalizationIncentive;
472: 
473:         redemptions[redeemer] = Redemption(address(0), 0, 0, 0, 0);
474: 
475:         uint256 assetsRedeemed = LiquidityPool(liquidityPool).redeem(amount, address(this), address(this)); // <= FOUND
476: 
477:         _transferETHAndWrapIfFailWithGasLimit(WETH, msg.sender, incentive, 2_300);
478: 
479:         if (expectedAssets >= assetsRedeemed) {
480:             _transferAssetsRedeemed(token, redeemer, assetsRedeemed);
481:         } else {
482:             _transferAssetsRedeemed(token, redeemer, expectedAssets);
483:             _executeERC20DirectTransfer(token, liquidityPool, assetsRedeemed - expectedAssets);
484:             assetsRedeemed = expectedAssets;
485:         }
486: 
487:         emit LiquidityPoolRouter__RedemptionFinalized(msg.sender, redeemer, liquidityPool, amount, assetsRedeemed); // <= FOUND
488:     }

[NonCritical-22] Missing events in sensitive functions

Resolution

Sensitive setter functions in smart contracts often alter critical state variables. Without events emitted in these functions, external observers or dApps cannot easily track or react to these state changes. Missing events can obscure contract activity, hampering transparency and making integration more challenging. To resolve this, incorporate appropriate event emissions within these functions. Events offer an efficient way to log crucial changes, aiding in real-time tracking and post-transaction verification.

Num of instances: 2

Findings

Click to show findings

['262']

262:     function setVrfParameters(VrfParameters memory _vrfParameters) external onlyOwner { // <= FOUND
263:         _setVrfParameters(_vrfParameters);
264:     }

['270']

270:     function setFinalizationParams( // <= FOUND
271:         uint80 _timelockDelay,
272:         uint80 _finalizationForAllDelay,
273:         uint80 _finalizationIncentive
274:     ) external onlyOwner {
275:         _setFinalizationParams(_timelockDelay, _finalizationForAllDelay, _finalizationIncentive);
276:     }

[NonCritical-23] Unchecked increments can overflow

Resolution

Unchecked increments in variables can lead to overflow, causing values to wrap around unexpectedly. This can disrupt contract logic. Always validate before incrementing.

Num of instances: 1

Findings

Click to show findings

['598']

598:              unchecked {
599:                     ++count; // <= FOUND
600:                 }

[NonCritical-24] Avoid mutating function parameters

Resolution

Function parameters in Solidity are passed by value, meaning they are essentially local copies. Mutating them can lead to confusion and errors because the changes don't persist outside the function. By keeping function parameters immutable, you ensure clarity in code behavior, preventing unintended side-effects. If you need to modify a value based on a parameter, use a local variable inside the function, leaving the original parameter unaltered. By adhering to this practice, you maintain a clear distinction between input data and the internal processing logic, improving code readability and reducing the potential for bugs.

Num of instances: 7

Findings

Click to show findings

['61']

61:     function transferPayoutToPlayer(
62:         address game,
63:         uint256 amount,
64:         address receiver
65:     ) external nonReentrant whenNotPaused {
66:         _onlyGameConfigurationManager();
67:         address currency = asset();
68:         uint256 balance = IERC20(currency).balanceOf(address(this));
69:         if (balance < amount) {
70:             emit InsufficientFundsForPayout(game, receiver, currency, amount - balance);
71:             amount = balance; // <= FOUND
72:         }
73:         _executeERC20DirectTransfer(currency, receiver, amount);
74:         emit PayoutTransferred(game, receiver, currency, amount);
75:     }

['55']

55:     function transferPayoutToPlayer(
56:         address game,
57:         uint256 amount,
58:         address receiver
59:     ) external nonReentrant whenNotPaused {
60:         _onlyGameConfigurationManager();
61: 
62:         address weth = asset();
63: 
64:         uint256 balance = IERC20(weth).balanceOf(address(this));
65:         if (balance < amount) {
66:             emit InsufficientFundsForPayout(game, receiver, address(0), amount - balance);
67:             amount = balance; // <= FOUND
68:         }
69: 
70:         IWETH(weth).withdraw(amount);
71: 
72:         _transferETHAndWrapIfFailWithGasLimit(weth, receiver, amount, 2_300);
73: 
74:         emit PayoutTransferred(game, receiver, address(0), amount);
75:     }

['181']

181:     function _liquidityPoolBalance(address currency) internal view returns (uint256 balance) { // <= FOUND
182:         address liquidityPool = _getGameLiquidityPool(currency);
183:         if (currency == address(0)) {
184:             currency = WETH; // <= FOUND
185:         }
186:         balance = IERC20(currency).balanceOf(liquidityPool);
187:     }

['396']

396:     function _dropTheBall(
397:         uint256 riskLevel,
398:         uint256 rowCount,
399:         uint256 randomWord
400:     ) private view returns (uint256 result, uint256 multiplier, uint256 randomWordForNextRound) {
401:         int256 movement;
402:         for (uint256 i; i < rowCount; ++i) {
403:             if (randomWord % 2 == 0) {
404:                 movement -= 1;
405:             } else {
406:                 movement += 1;
407:                 result |= (1 << i);
408:             }
409: 
410:             randomWord = uint256(keccak256(abi.encode(randomWord))); // <= FOUND
411:         }
412: 
413:         uint256 bin = uint256(movement + int256(rowCount)) / 2;
414:         multiplier = multipliers[_multiplierKey(riskLevel, rowCount, bin)];
415:         randomWordForNextRound = randomWord;
416:     }

['249']

249:     function _handlePayout(
250:         address player,
251:         Game__GameParams storage params,
252:         uint256 numberOfRoundsPlayed,
253:         uint256 payout,
254:         uint256 protocolFee
255:     ) internal {
256:         payout += params.playAmountPerRound * (params.numberOfRounds - numberOfRoundsPlayed); // <= FOUND
257:         _transferPlayAmountToPool(params.currency, params.playAmountPerRound * params.numberOfRounds);
258:         if (payout > 0) {
259:             GAME_CONFIGURATION_MANAGER.transferPayoutToPlayer(params.currency, payout, player);
260:         }
261:         if (protocolFee > 0) {
262:             GAME_CONFIGURATION_MANAGER.transferProtocolFee(params.currency, protocolFee);
263:         }
264:     }

['284']

284:     function depositETH(uint256 amount) external payable nonReentrant whenNotPaused { // <= FOUND
285:         address liquidityPool = _getLiquidityPoolOrRevert(WETH);
286: 
287:         if (amount + finalizationParams.finalizationIncentive != msg.value) {
288:             revert LiquidityPoolRouter__FinalizationIncentiveNotPaid();
289:         }
290: 
291:         uint256 depositFee = (amount * DEPOSIT_FEE_BASIS_POINTS) / 10_000;
292:         amount -= depositFee; // <= FOUND
293: 
294:         _validateDepositAmount(liquidityPool, amount);
295: 
296:         if (deposits[msg.sender].amount != 0) {
297:             revert LiquidityPoolRouter__OngoingDeposit();
298:         }
299: 
300:         _transferETHAndWrapIfFailWithGasLimit(WETH, owner, depositFee, gasleft());
301: 
302:         uint256 expectedShares = ERC4626(liquidityPool).previewDeposit(amount);
303: 
304:         deposits[msg.sender] = Deposit(
305:             liquidityPool,
306:             amount,
307:             expectedShares,
308:             block.timestamp,
309:             finalizationParams.finalizationIncentive
310:         );
311:         pendingDeposits[liquidityPool] += amount;
312: 
313:         emit LiquidityPoolRouter__DepositInitialized(
314:             msg.sender,
315:             liquidityPool,
316:             amount + depositFee,
317:             expectedShares,
318:             finalizationParams.finalizationIncentive
319:         );
320:     }

['329']

329:     function deposit(address token, uint256 amount) external payable nonReentrant whenNotPaused { // <= FOUND
330:         address liquidityPool = _getLiquidityPoolOrRevert(token);
331: 
332:         _validateFinalizationIncentivePayment();
333: 
334:         uint256 depositFee = (amount * DEPOSIT_FEE_BASIS_POINTS) / 10_000;
335:         amount -= depositFee; // <= FOUND
336: 
337:         _validateDepositAmount(liquidityPool, amount);
338: 
339:         if (deposits[msg.sender].amount != 0) {
340:             revert LiquidityPoolRouter__OngoingDeposit();
341:         }
342: 
343:         TRANSFER_MANAGER.transferERC20(token, msg.sender, address(this), amount);
344:         TRANSFER_MANAGER.transferERC20(token, msg.sender, owner, depositFee);
345: 
346:         uint256 expectedShares = ERC4626(liquidityPool).previewDeposit(amount);
347: 
348:         deposits[msg.sender] = Deposit(
349:             liquidityPool,
350:             amount,
351:             expectedShares,
352:             block.timestamp,
353:             finalizationParams.finalizationIncentive
354:         );
355:         pendingDeposits[liquidityPool] += amount;
356: 
357:         emit LiquidityPoolRouter__DepositInitialized(
358:             msg.sender,
359:             liquidityPool,
360:             amount + depositFee,
361:             expectedShares,
362:             finalizationParams.finalizationIncentive
363:         );
364:     }

[NonCritical-25] Avoid hard coding gasLimit values

Resolution

Hardcoding gasLimit values in smart contracts can be problematic. Gas requirements can change due to network conditions or contract updates. If set too low, transactions may fail; if set too high, unnecessary costs might be incurred. Moreover, Ethereum's gas dynamics and costs are subject to change with network upgrades, like the transition to Ethereum 2.0. Instead of hardcoding, dynamically estimate gas or provide mechanisms for administrators to update gas limits. This ensures flexibility and avoids transaction failures or inefficiencies due to outdated gas settings. Always remember to test thoroughly after any changes to gas-related logic.

Num of instances: 2

Findings

Click to show findings

['144']

144:         VrfParameters memory _vrfParameters = VrfParameters({ // <= FOUND
145:             coordinator: _vrfCoordinator,
146:             subscriptionId: _subscriptionId,
147:             callbackGasLimit: 2_500_000, // <= FOUND
148:             minimumRequestConfirmations: 3,
149:             vrfFee: 0.0003 ether,
150:             keyHash: _keyHash
151:         });

['366']

366:         vrfParameters.callbackGasLimit = _vrfParameters.callbackGasLimit; // <= FOUND

[NonCritical-26] Contracts use both += 1 and ++ (-- and -= 1)

Resolution

For consistency consider only using one of these incrementing methods.

Num of instances: 1

Findings

Click to show findings

['13']

13: contract LaserBlast is Game {
14:     
17:     uint256 private constant MINIMUM_NUMBER_OF_ROWS = 8;
18: 
22:     uint256 private constant MAXIMUM_NUMBER_OF_ROWS = 16;
23: 
27:     uint256 private constant STARTING_RISK_LEVEL = 1;
28: 
32:     uint256 private constant HIGHEST_RISK_LEVEL = 3;
33: 
40:     struct LaserBlast__Game {
41:         Game__GameParams params;
42:         uint128 riskLevel;
43:         uint128 rowCount;
44:     }
45: 
46:     mapping(address player => LaserBlast__Game) public games;
47: 
51:     mapping(bytes32 key => uint256 multiplier) private multipliers;
52: 
56:     uint256[9][3] private kellyFractions;
57: 
58:     event LaserBlast__GameCreated(
59:         uint256 blockNumber,
60:         address player,
61:         uint256 numberOfRounds,
62:         uint256 playAmountPerRound,
63:         address currency,
64:         int256 stopGain,
65:         int256 stopLoss,
66:         uint256 riskLevel,
67:         uint256 rowCount
68:     );
69: 
70:     event LaserBlast__GameCompleted(
71:         uint256 blockNumber,
72:         address player,
73:         uint256[] results,
74:         uint256[] payouts,
75:         uint256 numberOfRoundsPlayed,
76:         uint256 protocolFee,
77:         uint256 liquidityPoolFee
78:     );
79:     event LaserBlast__MultipliersSet(uint256 riskLevel, uint256 rowCount, uint256[] multipliers);
80: 
81:     error LaserBlast__InvalidRiskLevel();
82:     error LaserBlast__InvalidRowCount();
83:     error LaserBlast__MultiplierAlreadySet();
84:     error LaserBlast__BinsCountMustBeOneHigherThanRowCount();
85: 
95:     constructor(
96:         address _gameConfigurationManager,
97:         address _transferManager,
98:         address _weth,
99:         address _vrfCoordinator,
100:         address _blast,
101:         address _usdb,
102:         address _owner
103:     ) Game(_gameConfigurationManager, _transferManager, _weth, _vrfCoordinator, _blast, _usdb, _owner) {
104:         kellyFractions[0] = [41300, 25900, 31400, 29500, 63700, 49000, 49700, 47300, 28800];
105:         kellyFractions[1] = [6320, 9480, 11300, 9920, 10920, 10280, 9460, 8490, 7350];
106:         kellyFractions[2] = [4049, 2689, 1563, 1599, 1247, 869, 547, 634, 369];
107:     }
108: 
117:     function setMultipliers(uint128 riskLevel, uint128 rowCount, uint256[] memory _multipliers) external onlyOwner {
118:         _validateRiskLevel(riskLevel);
119:         _validateRowCount(rowCount);
120: 
121:         uint256 binsCount = _multipliers.length;
122: 
123:         if (binsCount != rowCount + 1) {
124:             revert LaserBlast__BinsCountMustBeOneHigherThanRowCount();
125:         }
126: 
127:         for (uint256 i; i < binsCount; ++i) { // <= FOUND
128:             uint256 multiplier = _multipliers[i];
129:             if (multiplier == 0) {
130:                 revert Game__ZeroMultiplier();
131:             }
132: 
133:             bytes32 key = _multiplierKey(riskLevel, rowCount, i);
134: 
135:             if (multipliers[key] != 0) {
136:                 revert LaserBlast__MultiplierAlreadySet();
137:             }
138: 
139:             multipliers[key] = multiplier;
140:         }
141: 
142:         emit LaserBlast__MultipliersSet(riskLevel, rowCount, _multipliers);
143:     }
144: 
155:     function play(
156:         uint16 numberOfRounds,
157:         uint256 playAmountPerRound,
158:         address currency,
159:         int256 stopGain,
160:         int256 stopLoss,
161:         uint128 riskLevel,
162:         uint128 rowCount
163:     ) external payable nonReentrant {
164:         _validateNumberOfRoundsAndPlayAmountPerRound(numberOfRounds, playAmountPerRound);
165:         _validateNoOngoingRound(games[msg.sender].params.numberOfRounds);
166:         _validateStopGainAndLoss(stopGain, stopLoss);
167:         _validateMultiplierIsSet(riskLevel, rowCount);
168: 
169:         uint256 _maxPlayAmountPerGame = maxPlayAmountPerGame(currency, riskLevel, rowCount);
170:         uint256 totalPlayAmount = playAmountPerRound * numberOfRounds;
171: 
172:         if (totalPlayAmount > _maxPlayAmountPerGame) {
173:             revert Game__PlayAmountPerRoundTooHigh();
174:         }
175: 
176:         if (totalPlayAmount < _minPlayAmountPerGame(_maxPlayAmountPerGame)) {
177:             revert Game__PlayAmountPerRoundTooLow();
178:         }
179: 
180:         
181:         
182:         
183:         _getGameLiquidityPool(currency);
184: 
185:         uint256 vrfFee = _requestRandomness();
186: 
187:         _escrowPlayAmountAndChargeVrfFee(currency, numberOfRounds, playAmountPerRound, vrfFee);
188: 
189:         games[msg.sender] = LaserBlast__Game({
190:             params: Game__GameParams({
191:                 blockNumber: uint40(block.number),
192:                 numberOfRounds: numberOfRounds,
193:                 playAmountPerRound: playAmountPerRound,
194:                 currency: currency,
195:                 stopGain: stopGain,
196:                 stopLoss: stopLoss,
197:                 randomnessRequestedAt: uint40(block.timestamp),
198:                 vrfFee: vrfFee
199:             }),
200:             riskLevel: riskLevel,
201:             rowCount: rowCount
202:         });
203: 
204:         emit LaserBlast__GameCreated(
205:             block.number,
206:             msg.sender,
207:             numberOfRounds,
208:             playAmountPerRound,
209:             currency,
210:             stopGain,
211:             stopLoss,
212:             riskLevel,
213:             rowCount
214:         );
215:     }
216: 
220:     function refund() external nonReentrant {
221:         _refund(games[msg.sender].params);
222:         _deleteGame(msg.sender);
223:     }
224: 
231:     function getMultipliers(uint128 riskLevel, uint128 rowCount) external view returns (uint256[] memory _multipliers) {
232:         _validateRiskLevel(riskLevel);
233:         _validateRowCount(rowCount);
234: 
235:         uint256 binsCount = rowCount + 1;
236:         _multipliers = new uint256[](binsCount);
237: 
238:         for (uint256 i; i < binsCount; ++i) { // <= FOUND
239:             _multipliers[i] = multipliers[_multiplierKey(riskLevel, rowCount, i)];
240:         }
241:     }
242: 
254:     function maxPlayAmountPerGame(
255:         address currency,
256:         uint128 riskLevel,
257:         uint128 rowCount
258:     ) public view returns (uint256 maxPlayAmount) {
259:         _validateRiskLevel(riskLevel);
260:         _validateRowCount(rowCount);
261: 
262:         
263: 
268:         maxPlayAmount =
269:             (_liquidityPoolBalance(currency) *
270:                 kellyFractions[riskLevel - STARTING_RISK_LEVEL][rowCount - MINIMUM_NUMBER_OF_ROWS] *
271:                 GAME_CONFIGURATION_MANAGER.kellyFractionBasisPoints(address(this))) /
272:             1e10;
273:     }
274: 
282:     function minPlayAmountPerGame(
283:         address currency,
284:         uint128 riskLevel,
285:         uint128 rowCount
286:     ) public view returns (uint256 minPlayAmount) {
287:         minPlayAmount = _minPlayAmountPerGame(maxPlayAmountPerGame(currency, riskLevel, rowCount));
288:     }
289: 
294:     function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override nonReentrant {
295:         address player = randomnessRequests[requestId];
296: 
297:         if (player != address(0)) {
298:             LaserBlast__Game storage game = games[player];
299:             if (_hasLiquidityPool(game.params.currency)) {
300:                 if (_vrfResponseIsNotTooLate(game.params.randomnessRequestedAt)) {
301:                     randomnessRequests[requestId] = address(0);
302: 
303:                     RunningGameState memory runningGameState;
304:                     runningGameState.payouts = new uint256[](game.params.numberOfRounds);
305:                     runningGameState.randomWord = randomWords[0];
306:                     uint256[] memory results = new uint256[](game.params.numberOfRounds);
307: 
308:                     IGameConfigurationManager.FeeSplit memory feeSplit = GAME_CONFIGURATION_MANAGER.getFeeSplit(
309:                         address(this)
310:                     );
311: 
312:                     uint256 adjustedLiquidityPoolFeeBasisPoints = _applyMultiplierToLiquidityPoolFeeBasisPoints(
313:                         feeSplit.liquidityPoolFeeBasisPoints,
314:                         game.riskLevel
315:                     );
316: 
317:                     Fee memory fee;
318:                     uint256 playAmountPerRound = game.params.playAmountPerRound;
319: 
320:                     for (
321:                         ;
322:                         runningGameState.numberOfRoundsPlayed < game.params.numberOfRounds;
323:                         ++runningGameState.numberOfRoundsPlayed // <= FOUND
324:                     ) {
325:                         if (
326:                             _stopGainOrStopLossHit(
327:                                 game.params.stopGain,
328:                                 game.params.stopLoss,
329:                                 runningGameState.netAmount
330:                             )
331:                         ) {
332:                             break;
333:                         }
334: 
335:                         (uint256 result, uint256 multiplier, uint256 randomWordForNextRound) = _dropTheBall(
336:                             game.riskLevel,
337:                             game.rowCount,
338:                             runningGameState.randomWord
339:                         );
340: 
341:                         uint256 protocolFee = (playAmountPerRound * multiplier * feeSplit.protocolFeeBasisPoints) / 1e8;
342:                         uint256 liquidityPoolFee = (playAmountPerRound *
343:                             multiplier *
344:                             adjustedLiquidityPoolFeeBasisPoints) / 1e8;
345: 
346:                         runningGameState.payouts[runningGameState.numberOfRoundsPlayed] =
347:                             (playAmountPerRound * multiplier) /
348:                             10_000 -
349:                             protocolFee -
350:                             liquidityPoolFee;
351:                         runningGameState.payout += runningGameState.payouts[runningGameState.numberOfRoundsPlayed];
352:                         runningGameState.netAmount += (int256(
353:                             runningGameState.payouts[runningGameState.numberOfRoundsPlayed]
354:                         ) - int256(playAmountPerRound));
355:                         fee.protocolFee += protocolFee;
356:                         fee.liquidityPoolFee += liquidityPoolFee;
357:                         results[runningGameState.numberOfRoundsPlayed] = result;
358: 
359:                         runningGameState.randomWord = randomWordForNextRound;
360:                     }
361: 
362:                     _handlePayout(
363:                         player,
364:                         game.params,
365:                         runningGameState.numberOfRoundsPlayed,
366:                         runningGameState.payout,
367:                         fee.protocolFee
368:                     );
369:                     _transferVrfFee(game.params.vrfFee);
370: 
371:                     emit LaserBlast__GameCompleted(
372:                         game.params.blockNumber,
373:                         player,
374:                         results,
375:                         runningGameState.payouts,
376:                         runningGameState.numberOfRoundsPlayed,
377:                         fee.protocolFee,
378:                         fee.liquidityPoolFee
379:                     );
380: 
381:                     _deleteGame(player);
382:                 }
383:             }
384:         }
385:     }
386: 
396:     function _dropTheBall(
397:         uint256 riskLevel,
398:         uint256 rowCount,
399:         uint256 randomWord
400:     ) private view returns (uint256 result, uint256 multiplier, uint256 randomWordForNextRound) {
401:         int256 movement;
402:         for (uint256 i; i < rowCount; ++i) { // <= FOUND
403:             if (randomWord % 2 == 0) {
404:                 movement -= 1;
405:             } else {
406:                 movement += 1; // <= FOUND
407:                 result |= (1 << i);
408:             }
409: 
410:             randomWord = uint256(keccak256(abi.encode(randomWord)));
411:         }
412: 
413:         uint256 bin = uint256(movement + int256(rowCount)) / 2;
414:         multiplier = multipliers[_multiplierKey(riskLevel, rowCount, bin)];
415:         randomWordForNextRound = randomWord;
416:     }
417: 
421:     function _deleteGame(address player) private {
422:         games[player] = LaserBlast__Game({
423:             params: Game__GameParams({
424:                 blockNumber: 0,
425:                 numberOfRounds: 0,
426:                 playAmountPerRound: 0,
427:                 currency: address(0),
428:                 stopGain: 0,
429:                 stopLoss: 0,
430:                 randomnessRequestedAt: 0,
431:                 vrfFee: 0
432:             }),
433:             riskLevel: 0,
434:             rowCount: 0
435:         });
436:     }
437: 
441:     function _validateRiskLevel(uint256 riskLevel) private pure {
442:         if (riskLevel < STARTING_RISK_LEVEL || riskLevel > HIGHEST_RISK_LEVEL) {
443:             revert LaserBlast__InvalidRiskLevel();
444:         }
445:     }
446: 
450:     function _validateRowCount(uint256 rowCount) private pure {
451:         if (rowCount < MINIMUM_NUMBER_OF_ROWS || rowCount > MAXIMUM_NUMBER_OF_ROWS) {
452:             revert LaserBlast__InvalidRowCount();
453:         }
454:     }
455: 
462:     function _validateMultiplierIsSet(uint256 riskLevel, uint256 rowCount) private view {
463:         if (multipliers[_multiplierKey(riskLevel, rowCount, 0)] == 0) {
464:             revert Game__ZeroMultiplier();
465:         }
466:     }
467: 
475:     function _multiplierKey(uint256 riskLevel, uint256 rowCount, uint256 bin) private pure returns (bytes32 key) {
476:         key = keccak256(abi.encode(riskLevel, rowCount, bin));
477:     }
478: 
485:     function _applyMultiplierToLiquidityPoolFeeBasisPoints(
486:         uint256 liquidityPoolFeeBasisPoints,
487:         uint256 riskLevel
488:     ) private pure returns (uint256 adjustedLiquidityPoolFeeBasisPoints) {
489:         if (riskLevel == 1) {
490:             adjustedLiquidityPoolFeeBasisPoints = liquidityPoolFeeBasisPoints;
491:         } else if (riskLevel == 2) {
492:             adjustedLiquidityPoolFeeBasisPoints = (liquidityPoolFeeBasisPoints * 3) / 2;
493:         } else if (riskLevel == 3) {
494:             adjustedLiquidityPoolFeeBasisPoints = liquidityPoolFeeBasisPoints * 2;
495:         }
496:     }
497: }

[NonCritical-27] Long numbers should include underscores to improve readability and prevent typos

Resolution

A large number such as 2000000 is far more readable as 2_000_000, this will help prevent unintended bugs in the code

Num of instances: 2

Findings

Click to show findings

['104']

104:         kellyFractions[0] = [41300, 25900, 31400, 29500, 63700, 49000, 49700, 47300, 28800]; // <= FOUND

['105']

105:         kellyFractions[1] = [6320, 9480, 11300, 9920, 10920, 10280, 9460, 8490, 7350]; // <= FOUND

[NonCritical-28] Avoid declaring variables with the names of defined functions within the project

Resolution

Having such variables can create confusion in both developers and in users of the project. Consider renaming these variables to improve code clarity.

Num of instances: 1

Findings

Click to show findings

['393']

393:     
398:     function _minPlayAmountPerGame(uint256 maxPlayAmountPerGame) internal pure returns (uint256 minPlayAmountPerGame) { // <= FOUND

[NonCritical-29] Constructors should emit an event

Resolution

Emitting an event in a constructor of a smart contract provides transparency and traceability in blockchain applications. This event logs the contract’s creation, aiding in monitoring and verifying contract deployment. Although constructors are executed only once, the emitted event ensures the contract's initialization is recorded on the blockchain.

Num of instances: 7

Findings

Click to show findings

['122']

122:     constructor( // <= FOUND
123:         address _gameConfigurationManager,
124:         address _transferManager,
125:         address _weth,
126:         address _vrfCoordinator,
127:         address _blast,
128:         address _usdb,
129:         address _owner,
130:         uint256[25] memory _maximumRevealableTiles
131:     ) Game(_gameConfigurationManager, _transferManager, _weth, _vrfCoordinator, _blast, _usdb, _owner) {
132:         uint256 maximumRevealableTilesLength = _maximumRevealableTiles.length;
133:         for (uint256 i; i < maximumRevealableTilesLength; ++i) {
134:             maximumRevealableTiles[i] = _maximumRevealableTiles[i];
135:         }
136: 
137:         for (uint256 lavasCount = 1; lavasCount < GRID_SIZE; ++lavasCount) {
138:             uint256 maximumRevealableTilesCount = _maximumRevealableTiles[lavasCount];
139:             for (
140:                 uint256 revealedTilesCount = 1;
141:                 revealedTilesCount <= maximumRevealableTilesCount;
142:                 ++revealedTilesCount
143:             ) {
144:                 uint256 numerator = 1;
145:                 uint256 denominator = 1;
146:                 for (
147:                     uint256 revealedTilesCountSoFar;
148:                     revealedTilesCountSoFar < revealedTilesCount;
149:                     ++revealedTilesCountSoFar
150:                 ) {
151:                     numerator *= (GRID_SIZE - lavasCount - revealedTilesCountSoFar);
152:                     denominator *= (GRID_SIZE - revealedTilesCountSoFar);
153:                 }
154:                 winProbabilities[lavasCount][revealedTilesCount] = (numerator * 1e18) / denominator;
155:             }
156:         }
157:     }

['29']

29:     constructor( // <= FOUND
30:         string memory _name,
31:         string memory _symbol,
32:         address _owner,
33:         address _asset,
34:         bool _isUSDB,
35:         address _gameConfigurationManager,
36:         address _liquidityPoolRouter,
37:         address _blast,
38:         address _blastPoints,
39:         address _blastPointsOperator
40:     )
41:         LiquidityPool(
42:             _name,
43:             _symbol,
44:             _owner,
45:             _asset,
46:             _gameConfigurationManager,
47:             _liquidityPoolRouter,
48:             _blast,
49:             _blastPoints,
50:             _blastPointsOperator
51:         )
52:     {
53:         if (_isUSDB) {
54:             IERC20Rebasing(_asset).configure(YieldMode.CLAIMABLE);
55:         }
56:     }

['28']

28:     constructor( // <= FOUND
29:         address _owner,
30:         address _weth,
31:         address _gameConfigurationManager,
32:         address _liquidityPoolRouter,
33:         address _blast,
34:         address _blastPoints,
35:         address _blastPointsOperator
36:     )
37:         LiquidityPool(
38:             "YOLO Games ETH",
39:             "yETH",
40:             _owner,
41:             _weth,
42:             _gameConfigurationManager,
43:             _liquidityPoolRouter,
44:             _blast,
45:             _blastPoints,
46:             _blastPointsOperator
47:         )
48:     {
49:         IERC20Rebasing(_weth).configure(YieldMode.CLAIMABLE);
50:     }

['115']

115:     constructor( // <= FOUND
116:         address _gameConfigurationManager,
117:         address _transferManager,
118:         address _weth,
119:         address _vrfCoordinator,
120:         address _blast,
121:         address _usdb,
122:         address _owner
123:     ) VRFConsumerBaseV2(_vrfCoordinator) OwnableTwoSteps(_owner) {
124:         GAME_CONFIGURATION_MANAGER = IGameConfigurationManager(_gameConfigurationManager);
125:         TRANSFER_MANAGER = ITransferManager(_transferManager);
126:         WETH = _weth;
127:         USDB = _usdb;
128: 
129:         (address coordinator, , , , , ) = GAME_CONFIGURATION_MANAGER.vrfParameters();
130:         if (coordinator != _vrfCoordinator) {
131:             revert Game__WrongVrfCoordinator();
132:         }
133: 
134:         IBlast(_blast).configure(IBlast__YieldMode.CLAIMABLE, GasMode.CLAIMABLE, _owner);
135:         IERC20Rebasing(_usdb).configure(IERC20Rebasing__YieldMode.CLAIMABLE);
136:     }

['95']

95:     constructor( // <= FOUND
96:         address _gameConfigurationManager,
97:         address _transferManager,
98:         address _weth,
99:         address _vrfCoordinator,
100:         address _blast,
101:         address _usdb,
102:         address _owner
103:     ) Game(_gameConfigurationManager, _transferManager, _weth, _vrfCoordinator, _blast, _usdb, _owner) {
104:         kellyFractions[0] = [41300, 25900, 31400, 29500, 63700, 49000, 49700, 47300, 28800];
105:         kellyFractions[1] = [6320, 9480, 11300, 9920, 10920, 10280, 9460, 8490, 7350];
106:         kellyFractions[2] = [4049, 2689, 1563, 1599, 1247, 869, 547, 634, 369];
107:     }

['53']

53:     constructor( // <= FOUND
54:         string memory _name,
55:         string memory _symbol,
56:         address _owner,
57:         address _asset,
58:         address _gameConfigurationManager,
59:         address _liquidityPoolRouter,
60:         address _blast,
61:         address _blastPoints,
62:         address _blastPointsOperator
63:     ) ERC20(_name, _symbol) ERC4626(IERC20(_asset)) OwnableTwoSteps(_owner) {
64:         GAME_CONFIGURATION_MANAGER = _gameConfigurationManager;
65:         LIQUIDITY_POOL_ROUTER = _liquidityPoolRouter;
66: 
67:         IBlast(_blast).configure(YieldMode.CLAIMABLE, GasMode.CLAIMABLE, _owner);
68:         IBlastPoints(_blastPoints).configurePointsOperator(_blastPointsOperator);
69:     }

['202']

202:     constructor( // <= FOUND
203:         address _owner,
204:         address _weth,
205:         address _usdb,
206:         address _transferManager,
207:         address _blast,
208:         address _blastPoints,
209:         address _blastPointsOperator
210:     ) OwnableTwoSteps(_owner) {
211:         WETH = _weth;
212:         USDB = _usdb;
213:         TRANSFER_MANAGER = ITransferManager(_transferManager);
214: 
215:         _setFinalizationParams(10 seconds, 5 minutes, 0.0003 ether);
216: 
217:         IBlast(_blast).configure(IBlast__YieldMode.CLAIMABLE, IBlast__GasMode.CLAIMABLE, _owner);
218:         IERC20Rebasing(_weth).configure(IERC20Rebasing__YieldMode.CLAIMABLE);
219:         IERC20Rebasing(_usdb).configure(IERC20Rebasing__YieldMode.CLAIMABLE);
220:         IBlastPoints(_blastPoints).configurePointsOperator(_blastPointsOperator);
221:     }

[NonCritical-30] Constructor with array/string/bytes parameters has no empty array checks

Resolution

Failing to validate for empty array inputs in constructors can lead to vulnerabilities or logical errors in smart contracts. Constructors often initialize key contract settings, and arrays may represent crucial data like access controls, initial states, or configuration parameters. Without empty array checks, a contract might be deployed in an unintended state, lacking necessary data for its operations or security measures.

Num of instances: 3

Findings

Click to show findings

['29']

29:     constructor(
30:         string memory _name,
31:         string memory _symbol,
32:         address _owner,
33:         address _asset,
34:         bool _isUSDB,
35:         address _gameConfigurationManager,
36:         address _liquidityPoolRouter,
37:         address _blast,
38:         address _blastPoints,
39:         address _blastPointsOperator
40:     )
41:         LiquidityPool(
42:             _name,
43:             _symbol,
44:             _owner,
45:             _asset,
46:             _gameConfigurationManager,
47:             _liquidityPoolRouter,
48:             _blast,
49:             _blastPoints,
50:             _blastPointsOperator
51:         )
52:     {
53:         if (_isUSDB) {
54:             IERC20Rebasing(_asset).configure(YieldMode.CLAIMABLE);
55:         }
56:     }

['53']

53:     constructor(
54:         string memory _name,
55:         string memory _symbol,
56:         address _owner,
57:         address _asset,
58:         address _gameConfigurationManager,
59:         address _liquidityPoolRouter,
60:         address _blast,
61:         address _blastPoints,
62:         address _blastPointsOperator
63:     ) ERC20(_name, _symbol) ERC4626(IERC20(_asset)) OwnableTwoSteps(_owner) {
64:         GAME_CONFIGURATION_MANAGER = _gameConfigurationManager;
65:         LIQUIDITY_POOL_ROUTER = _liquidityPoolRouter;
66: 
67:         IBlast(_blast).configure(YieldMode.CLAIMABLE, GasMode.CLAIMABLE, _owner);
68:         IBlastPoints(_blastPoints).configurePointsOperator(_blastPointsOperator);
69:     }

['126']

126:     constructor(
127:         address _owner,
128:         address _weth,
129:         address _blast,
130:         address _vrfFeeRecipient,
131:         address _protocolFeeRecipient,
132:         address _vrfCoordinator,
133:         bytes32 _keyHash,
134:         uint64 _subscriptionId
135:     ) OwnableTwoSteps(_owner) {
136:         WETH = _weth;
137: 
138:         vrfFeeRecipient = _vrfFeeRecipient;
139:         emit VrfFeeRecipientUpdated(_vrfFeeRecipient);
140: 
141:         protocolFeeRecipient = _protocolFeeRecipient;
142:         emit ProtocolFeeRecipientUpdated(_protocolFeeRecipient);
143: 
144:         VrfParameters memory _vrfParameters = VrfParameters({
145:             coordinator: _vrfCoordinator,
146:             subscriptionId: _subscriptionId,
147:             callbackGasLimit: 2_500_000,
148:             minimumRequestConfirmations: 3,
149:             vrfFee: 0.0003 ether,
150:             keyHash: _keyHash
151:         });
152:         _setVrfParameters(_vrfParameters);
153: 
154:         IBlast(_blast).configure(YieldMode.CLAIMABLE, GasMode.CLAIMABLE, _owner);
155:     }

[NonCritical-31] Errors should have parameters

Resolution

In Solidity, custom errors with parameters offer a gas-efficient way to convey detailed information about issues encountered during contract execution. Unlike revert messages, which are strings consuming more gas, custom errors defined with parameters allow developers to specify types and details of errors succinctly. This method enhances debugging, provides clearer insights into contract failures, and improves the developer's and end-user's understanding of what went wrong, all while optimizing for gas usage and maintaining contract efficiency.

Num of instances: 94

Findings

Click to show findings

['108']

108:     error DontFallIn__NoSelectedTiles(); // <= FOUND

['109']

109:     error DontFallIn__SelectedTilesExceededMaxSize(); // <= FOUND

['110']

110:     error DontFallIn__TilesAlreadyRevealed(); // <= FOUND

['90']

90:     error Game__InexactNativeTokensSupplied(); // <= FOUND

['91']

91:     error Game__InvalidMultiplier(); // <= FOUND

['92']

92:     error Game__InvalidStops(); // <= FOUND

['93']

93:     error Game__InvalidValue(); // <= FOUND

['94']

94:     error Game__LiquidityPoolConnected(); // <= FOUND

['95']

95:     error Game__NoLiquidityPool(); // <= FOUND

['96']

96:     error Game__NoOngoingRound(); // <= FOUND

['97']

97:     error Game__OngoingRound(); // <= FOUND

['98']

98:     error Game__PlayAmountPerRoundTooHigh(); // <= FOUND

['99']

99:     error Game__PlayAmountPerRoundTooLow(); // <= FOUND

['100']

100:     error Game__TooEarlyForARefund(); // <= FOUND

['101']

101:     error Game__TooManyRounds(); // <= FOUND

['102']

102:     error Game__WrongVrfCoordinator(); // <= FOUND

['103']

103:     error Game__ZeroMultiplier(); // <= FOUND

['104']

104:     error Game__ZeroNumberOfRounds(); // <= FOUND

['105']

105:     error Game__ZeroPlayAmountPerRound(); // <= FOUND

['106']

106:     error GameConfigurationManager__BasisPointsTooHigh(); // <= FOUND

['107']

107:     error GameConfigurationManager__ElapsedTimeRequiredForRefundIsTooLong(); // <= FOUND

['108']

108:     error GameConfigurationManager__ElapsedTimeRequiredForRefundIsTooShort(); // <= FOUND

['110']

110:     error GameConfigurationManager__GameLiquidityPoolConnectionRequestConfirmationIsTooEarly(); // <= FOUND

['111']

111:     error GameConfigurationManager__GameLiquidityPoolCurrencyMismatch(); // <= FOUND

['112']

112:     error GameConfigurationManager__NoGameLiquidityPoolConnectionRequest(); // <= FOUND

['113']

113:     error GameConfigurationManager__VrfCallbackGasLimitTooLow(); // <= FOUND

['114']

114:     error GameConfigurationManager__VrfMinimumRequestConfirmationsTooLow(); // <= FOUND

['81']

81:     error LaserBlast__InvalidRiskLevel(); // <= FOUND

['82']

82:     error LaserBlast__InvalidRowCount(); // <= FOUND

['83']

83:     error LaserBlast__MultiplierAlreadySet(); // <= FOUND

['84']

84:     error LaserBlast__BinsCountMustBeOneHigherThanRowCount(); // <= FOUND

['70']

70:     error LiquidityPoolRouter__DepositAmountTooHigh(); // <= FOUND

['71']

71:     error LiquidityPoolRouter__DepositAmountTooLow(); // <= FOUND

['72']

72:     error LiquidityPoolRouter__FinalizationForAllIsNotOpen(); // <= FOUND

['73']

73:     error LiquidityPoolRouter__FinalizationIncentiveNotPaid(); // <= FOUND

['74']

74:     error LiquidityPoolRouter__FinalizationIncentiveTooHigh(); // <= FOUND

['75']

75:     error LiquidityPoolRouter__FinalizationForAllDelayTooHigh(); // <= FOUND

['76']

76:     error LiquidityPoolRouter__InvalidTimelockDelay(); // <= FOUND

['77']

77:     error LiquidityPoolRouter__MaxDepositAmountTooHigh(); // <= FOUND

['78']

78:     error LiquidityPoolRouter__MinDepositAmountTooHigh(); // <= FOUND

['79']

79:     error LiquidityPoolRouter__NoLiquidityPoolForToken(); // <= FOUND

['80']

80:     error LiquidityPoolRouter__NoOngoingDeposit(); // <= FOUND

['81']

81:     error LiquidityPoolRouter__NoOngoingRedemption(); // <= FOUND

['82']

82:     error LiquidityPoolRouter__OngoingDeposit(); // <= FOUND

['83']

83:     error LiquidityPoolRouter__OngoingRedemption(); // <= FOUND

['84']

84:     error LiquidityPoolRouter__TimelockIsNotOver(); // <= FOUND

['85']

85:     error LiquidityPoolRouter__TokenAlreadyHasLiquidityPool(); // <= FOUND

['108']

108: error DontFallIn__NoSelectedTiles(); // <= FOUND

['109']

109: error DontFallIn__SelectedTilesExceededMaxSize(); // <= FOUND

['110']

110: error DontFallIn__TilesAlreadyRevealed(); // <= FOUND

['106']

106: error GameConfigurationManager__BasisPointsTooHigh(); // <= FOUND

['107']

107: error GameConfigurationManager__ElapsedTimeRequiredForRefundIsTooLong(); // <= FOUND

['108']

108: error GameConfigurationManager__ElapsedTimeRequiredForRefundIsTooShort(); // <= FOUND

['110']

110: error GameConfigurationManager__GameLiquidityPoolConnectionRequestConfirmationIsTooEarly(); // <= FOUND

['111']

111: error GameConfigurationManager__GameLiquidityPoolCurrencyMismatch(); // <= FOUND

['112']

112: error GameConfigurationManager__NoGameLiquidityPoolConnectionRequest(); // <= FOUND

['113']

113: error GameConfigurationManager__VrfCallbackGasLimitTooLow(); // <= FOUND

['114']

114: error GameConfigurationManager__VrfMinimumRequestConfirmationsTooLow(); // <= FOUND

['81']

81: error LaserBlast__InvalidRiskLevel(); // <= FOUND

['82']

82: error LaserBlast__InvalidRowCount(); // <= FOUND

['83']

83: error LaserBlast__MultiplierAlreadySet(); // <= FOUND

['