XYRO - audits

CertiKRepository at commit 0a08cfa

CertiK reviewed XYRO (XYRO - audits) on 10 Jan 2025: 43 findings, 0 critical; 34 marked fixed, 9 open.

No open critical findings
Open the reportOpen XYRO’s security record

Report hosted on skynet.certik.com.

Trustblock does not review code.

Findings43

Statuses are the security firm’s own, from its report.

SeverityOpenAcknowledgedFixed
Critical000
High303
Medium1010
Low5021
HighDue to Lack of Game State Check, `closeGame()` inFixed

In the `closeGame()` function, as long as the game state is "Created" or the current time is three days after the game's `endTime`, an attacker can call `closeGame()` to refund the initiator's deposit repeatedly: `OneVsOneExactPrice.sol` ```solidity=405 function closeGame(bytes32 gameId) public { GameInfo memory game = decodeData(gameId); require(game.initiator == msg.sender, "Wrong sender"); require( game.gameStatus == Status.Created || ( block.timestamp > game.endTime ? block.timestamp - game.endTime >= 3 days : false ), "Wrong status!" ); ITreasury(treasury).refund( games[gameId].depositAmount, game.initiator, gameId ); //rewrites status games[gameId].packedData2 = (games[gameId].packedData2 & ~(uint256(0xFF) << 208)) | (uint256(uint8(Status.Cancelled)) << 208); emit ExactPriceCancelled(gameId); } ``` As a result, when the current time is three days after the `endTime`, there is no need to check the game state, and the refund can be made to the initiator. This allows `closeGame()` to be called repeatedly.For games in the "started" state, repeated calls by the initiator will also "refund" the opponent's deposit amount to initiator. In addition, when the game's lock record in treasury is large enough, the lock amount can be continuously withdrawn through this function. By combining this with the vulnerability in `createGameWithPermit`, which allows the repeated creation of a game with the same `gameId`, low-value tokens can be exchanged for high-value tokens.

HighFailure to Reset `totalRakebackUp` and `totalRakeFixed

In the `UpDown` contract, the `finalizeGame` function is responsible for finalizing the game and distributing rewards. However, when the game is canceled due to either `UpPlayers.length == 0` or `DownPlayers.length == 0`, the function resets several global storage variables but fails to reset `totalRakebackUp` and `totalRakebackDown`. This oversight can lead to incorrect calculations and potential asset loss in subsequent game rounds. The critical section of the code is: ```solidity if (UpPlayers.length == 0 || DownPlayers.length == 0) { // ... refund logic ... packedData = 0; totalDepositsUp = 0; totalDepositsDown = 0; currentGameId = bytes32(0); return; } // ... other logic ... delete DownPlayers; emit UpDownCancelled(currentGameId); packedData = 0; currentGameId = bytes32(0); return; // ... other logic ... delete DownPlayers; delete UpPlayers; ITreasury(treasury).setGameFinished(currentGameId); currentGameId = bytes32(0); packedData = 0; totalDepositsUp = 0; totalDepositsDown = 0; ``` The same issue is present at the end of the `finalizeGame` function where `totalRakebackUp` and `totalRakebackDown` are not reset, potentially affecting future games.

HighThe `createGameWithPermit` Function in `OneVsOneEFixed

In this function, there is no check like the one in `createGame` and `createGameWithDeposit` where `require(games[gameId].packedData == 0, "Game exists")` is used to verify if the current `gameId` already exists. As a result, when two `createGameWithPermit` transactions with the same `endTime` and both being public games (opponent is `address(0)`) are processed in the same block, the first created game will be overwritten by the second. Consequently, the user's bet for the first game will be locked in the contract and cannot be refunded or withdrawn through the game. In addition, since the locked amount for the game corresponding to the `gameId` has increased before the overwrite, this can be exploited in combination with the vulnerability that allows `closeGame()` to be called multiple times. This can lead to the exchange of low-value tokens for high-value tokens.

Medium`startingPrice` Not Set to Zero When the Game IsFixed

In the case where `UpPlayers.length == 0 || DownPlayers.length == 0`, the game is invalid, and the game principal should be refunded to the players, followed by a return. However, before the return, `startingPrice` is not set to zero: `UpDown.sol` ```solidity=304 emit UpDownCancelled(currentGameId); packedData = 0; totalDepositsUp = 0; totalDepositsDown = 0; totalRakebackUp = 0; totalRakebackDown = 0; currentGameId = bytes32(0); return; ``` This will result in an existing `startingPrice` before the next game starts.

MediumFailure to Reset `totalDepositsUp` and `totalDepoFixed

When `uint192(finalPrice / 1e14) == _game.startingPrice`, after refunding the tokens to the user, the user's `depositAmounts` are not set to 0, and `totalDepositsUp` and `totalDepositsDown` are not reset to 0: `UpDown.sol` ```solidity=348 else if (uint192(finalPrice / 1e14) == _game.startingPrice) { for (uint i; i < UpPlayers.length; i++) { ITreasury(treasury).refund( depositAmounts[UpPlayers[i]], UpPlayers[i], currentGameId ); isParticipating[UpPlayers[i]] = false; } delete UpPlayers; for (uint i; i < DownPlayers.length; i++) { ITreasury(treasury).refund( depositAmounts[DownPlayers[i]], DownPlayers[i], currentGameId ); isParticipating[DownPlayers[i]] = false; } delete DownPlayers; emit UpDownCancelled(currentGameId); packedData = 0; currentGameId = bytes32(0); return; } ``` This will cause the variables from the current round to be included in the next round's game, leading to confusion. It may result in the `finalizeGame()` function for the next game being unable to execute.

MediumIncorrect `Rakeback` Calculation for Multiple EntFixed

In the `Treasury` contract, the `distributeBullseye` function is responsible for deducting the appropriate rakeback for each guess during reward distribution. However, if a player participates multiple times in a single round, the rakeback calculation may be incorrect. For instance, if userA participates twice with different guesses and userB participates once, and userA's second guess wins, both userA and userB are meant to retain one share of rakeback, with userA winning the reward. The issue arises when calculating `winnersRakeback`, as all of userA's rakeback entries are considered, including those from losing guesses. This results in insufficient rakeback for players to withdraw. ```solidity for (uint i = 0; i < 3; i++) { if (currentRates[i] != 0) { winnersRakeback += ITreasury(treasury).lockedRakeback( currentGameId, topPlayers[i] ); } } for (uint256 i = 0; i < 3; i++) { if (topPlayers[i] != address(0)) { if (currentRates[i] != 0) { ITreasury(treasury).distributeBullseye( currentRates[i], totalRakeback - winnersRakeback, topPlayers[i], currentGameId, topRakeback[i] ); } } } ```

MediumIncorrect Calculation of `rakeback` in `Bullseys`Fixed

The `playWithPermit` function of `Bullseye` contract incorrectly calculates and double-counts the rakeback. The line `uint256 rakeback = totalRakeback += ITreasury(treasury).depositAndLockWithPermit(...)` both assigns and adds the value of rakeback to `totalRakeback`, leading to an incorrect rakeback value being stored in `playerGuessData` and emitted in the `BullseyeNewPlayer` event. ```solidity function playWithPermit( uint256 assetPrice, ITreasury.PermitData calldata permitData ) public { ... @> uint256 rakeback = totalRakeback += ITreasury(treasury) .depositAndLockWithPermit( depositAmount, msg.sender, currentGameId, playerGuessData.length, permitData.deadline, permitData.v, permitData.r, permitData.s ); ... @> totalRakeback += rakeback; emit BullseyeNewPlayer( msg.sender, assetPrice, depositAmount, currentGameId, playerGuessData.length - 1, rakeback ); } ```

MediumMissing Minimum Deposit Requirement in `lock` FunFixed

The `lock` function is responsible for locking tokens for a game and is invoked by other functions like `playWithDeposit` and `createGameWithDeposit`. However, unlike the deposit functions `depositAndLock`, `depositAndLockWithPermit` etc , `lock` does not enforce the requirement that the amount must be greater than or equal to `minDepositAmount[token]`. This omission allows players to lock fewer tokens than intended, potentially undermining game mechanics. Below is the relevant code snippet: ```solidity function lock( uint256 amount, address from, bytes32 gameId, bool isRakeback ) public onlyRole(DISTRIBUTOR_ROLE) returns (uint256 rakeback) { address token = gameToken[gameId]; require(approvedTokens[token], "Unapproved token"); require(deposits[token][from] >= amount, "Insufficent deposit amount"); // Missing minDepositAmount[token] check if (isRakeback) { rakeback = calculateRakebackAmount(from, amount); lockedRakeback[gameId][from] += rakeback; } deposits[token][from] -= amount; locked[gameId] += amount; } ```

MediumNo Cap on FeesFixed

The `fee` variables in the contract have no set limits, allowing fees to potentially reach 100%. If this occurs, users would receive no tokens from the contract, resulting in a complete loss of their investment.

MediumOpponent's Assets Locked on Game Closure in `closFixed

In the `OneVsOneExactPrice` contract, the `closeGame` function allows the game's initiator to close the game and refund tokens when the game is in the `Created` status or when the current time is three days after `game.endTime`. However, if the game is `Started`, the function only refunds the initiator's assets and does not account for the opponent's assets. Furthermore, there are no other functions available to refund the opponent's assets, potentially causing their assets to be locked indefinitely. The relevant code snippet is: ```solidity function closeGame(bytes32 gameId) public { GameInfo memory game = decodeData(gameId); require(game.initiator == msg.sender, "Wrong sender"); require( game.gameStatus == Status.Created || ( block.timestamp > game.endTime ? block.timestamp - game.endTime >= 3 days : false ), "Wrong status!" ); ITreasury(treasury).refund( games[gameId].depositAmount, game.initiator, gameId ); //rewrites status games[gameId].packedData2 = (games[gameId].packedData2 & ~(uint256(0xFF) << 208)) | (uint256(uint8(Status.Cancelled)) << 208); emit ExactPriceCancelled(gameId); } ```

Show 10 more
MediumPossible Failure for Last Winner to Claim RewardsFixed

In the `finalizeGame()` function, the initiator fee is rounded only once during the calculation for the winning side: ```solidity=555 finalRate = ITreasury(treasury).calculateRate( games[gameId].totalDepositsTP - ((games[gameId].totalDepositsTP * initiatorFee) / FEE_DENOMINATOR), games[gameId].totalRakebackSL, gameId ); ``` However, in the `retrieveRewards()` function, each winner may have their initiator fee rounded when withdrawing, which can lead to multiple rounding in the total calculation: ```solidity=635 ITreasury(treasury).universalDistribute( msg.sender, depositAmounts[gameIds[i]][msg.sender] - ((depositAmounts[gameIds[i]][msg.sender] * initiatorFee) / FEE_DENOMINATOR), gameIds[i], games[gameIds[i]].finalRate ); ``` In this case, the total rewards withdrawn by each winner may exceed the amount stored in `locked[gameId]`, causing some users' funds (either rakeBack or prize money, depending on who withdraws the reward last) to be unable to be extracted from the treasury.

MediumPotential Incorrect Fee Calculation in `withdrawIFixed

In the `Treasury` contract, the `withdrawInitiatorFee` function is designed to calculate and distribute the initiator's fee from the game deposits. However, the formula used to calculate `withdrawnFees` is incorrect. The current calculation is: ```solidity withdrawnFees = (wonTeamDeposits + lostTeamDeposits * initiatorFee) / FEE_DENOMINATOR; ``` This formula erroneously applies the `initiatorFee` only to `lostTeamDeposits`,and the winning team will be charged a 0.01% fee, leading to an incorrect fee calculation. According to [xyro.io](https://xyro.io/onboarding/one-vs-one),the fee should be calculated based on the combined total of `wonTeamDeposits` and `lostTeamDeposits`, multiplied by the rate `initiatorFee/FEE_DENOMINATOR`. This error can result in an incorrect initiator fee, causing potential asset loss to the game initiator.

MediumUpDown Game Finalizing Failure by Missing `isParFixed

Due to forgetting to set `isParticipating` of `msg.sender` to `true`, `msg.sender` can play the same game round multiple times, with their `deposits` being set to the amount of their last bet: ```solidity=126 function playWithDeposit(bool isLong, uint256 depositAmount) public { require(depositAmount >= minDepositAmount, "Wrong deposit amount"); require(!isParticipating[msg.sender], "Already participating"); require( DownPlayers.length + UpPlayers.length + 1 <= maxPlayers, "Max player amount reached" ); GameInfo memory game = decodeData(); require( game.stopPredictAt > block.timestamp && (game.totalDepositsDown + depositAmount <= type(uint32).max || game.totalDepositsUp + depositAmount <= type(uint32).max), "Game is closed for new players" ); if (isLong) { //rewrites totalDepositsUp packedData = (packedData & ~(uint256(0xFFFFFFFF) << 168)) | ((depositAmount + game.totalDepositsUp) << 168); UpPlayers.push(msg.sender); } else { //rewrites totalDepositsDown packedData = (packedData & ~(uint256(0xFFFFFFFF) << 136)) | ((depositAmount + game.totalDepositsDown) << 136); DownPlayers.push(msg.sender); } depositAmounts[msg.sender] = depositAmount; ITreasury(treasury).lock(depositAmount, msg.sender); emit UpDownNewPlayer(msg.sender, isLong, depositAmount, currentGameId); } ``` An attacker can repeatedly place small bets (e.g., 1 USDT) and then place a larger bet (e.g., 2 USDT). As a result, all of their bets will be updated to the last bet's amount (2 USDT) during the game finalization.At this point, the treasury will revert due to an underflow when it tries to distribute funds, because the funds locked in the contract for that round of the game are insufficient. This results in the `finalizeGame()` transaction failing.

Low`refundWithFee` Should Not Calculate RakebackFixed

For refunds, rakeback should not be calculated and added to the user's deposits. This portion of the rakeback is redundant and could potentially cause an underflow in `locked[gameId]`. Since the current game, `OneVsOneExactPrice`, does not calculate rakeback, underflow will not occur for now. However, future games may encounter this issue.

LowAll Unstarted Games in` Setup` Are in the "CreateFixed

Since the state at the first position in the `enum` corresponds to the number 0, any unstarted game is in state 0, i.e., the "Created" state. This makes the game state checks in some functions of the contract ineffective.

LowConfirmation on Execution Time of `Setup` Game FiFixed

In the `Setup` contract, the `GAME_MASTER_ROLE` has the ability to finalize the game before the designated end time. Finalization is permissible when the game's status is set to `Created`, and the `finalPrice` falls within the range of `stopLossPrice` and `takeProfitPrice`. This behavior is inconsistent with other games such as `Bullseye`, `OneVsOneExactPrice`, and `UpDown`, where finalization is only allowed after the `game.endTime` has been reached.

LowInappropriate `exactRange` Value for 18 Decimal PFixed

In the `Bullseye` contract, the `exactRange` variable is used to determine if a player's guessed price is sufficiently close to the `finalPrice`, which in turn affects the distribution rate. Previously, the price was represented with 4 decimals, and the default `exactRange` was set to `50000`. However, in the current version, the price is represented with 18 decimals. Continuing to use `50000` as the default `exactRange` value may lead to incorrect evaluations due to the increased precision. This could potentially affect the fairness and accuracy of the game's outcome.

LowInconsistent `withdrawStatus` Update on RakebackFixed

The `retrieveRewards` function in the smart contract allows users to claim rewards based on game outcomes. However, when withdrawing rakeback assets, the function does not update the `withdrawStatus` for the user, which could lead to inconsistency in tracking withdrawal statuses. Below is the relevant code snippet: ```solidity if (withdrawStatus[gameIds[i]][msg.sender] == UserStatus.SL) { require( ITreasury(treasury).lockedRakeback( gameIds[i], msg.sender ) != 0, "You lost" ); ITreasury(treasury).withdrawRakebackSetup( gameIds[i], msg.sender ); // Missing update to withdrawStatus here } ``` Due to the weak validation of game status in `closeGame()`, if the official mistakenly calls `closeGame()` after `finalizeGame()`, it will allow the losing players to refund their principal in the game after receiving the rakeBack.

LowInherited Contracts Not Initialized In InitializeFixed

Contract `Treasury` extends `AccessControlUpgradeable`, but the extended contract is not initialized by the current contract. Generally, the initializer function of a contract should always call all the initializer functions of the contracts that it extends.

LowLack of `gameId` Existence Check in `createSetup(Fixed

`createSetup()` lacks a check for the existence of `gameId`. When a user creates two games with the same `gameId` at the same time, the second one will overwrite the first.

Show 10 more
LowLack of Checks in `setStartingPrice()` in `UpDownFixed

`UpDown.sol` ```solidity=234 function setStartingPrice( bytes memory unverifiedReport ) public onlyRole(GAME_MASTER_ROLE) { GameInfo memory game = decodeData(); require(block.timestamp >= game.stopPredictAt, "Too early"); require( UpPlayers.length != 0 || DownPlayers.length != 0, "Not enough players" ); address upkeep = ITreasury(treasury).upkeep(); (int192 startingPrice, uint32 priceTimestamp) = IDataStreamsVerifier( upkeep ).verifyReportWithTimestamp(unverifiedReport, game.feedNumber); require( block.timestamp - priceTimestamp <= 1 minutes, "Old chainlink report" ); packedData |= uint192(startingPrice / 1e14) << 104; emit UpDownStarted(startingPrice, currentGameId); } ``` Since `setStartingPrice` only requires `block.timestamp` to be greater than or equal to `game.stopPredictAt` and does not require `block.timestamp` to be less than `game.endTime`, this allows the game master to set the starting price after the game has ended. The function does not check if the `startingPrice` in `packedData` is 0, which allows the game master to repeatedly call the function to set the `startingPrice` multiple times after `game.stopPredictAt`. The function only checks `require(block.timestamp - priceTimestamp <= 1 minutes, "Old chainlink report");`, but does not restrict the time difference between `priceTimestamp` and `game.stopPredictAt`, which allows an expired price to be set as the starting price.

LowLack of End Time Validation in `Setup` Contract'sFixed

In the `Setup` contract, the process of finalizing a game involves decoding the `finalPrice` and `endTime` from the `unverifiedReport`. However, unlike other game contracts, the `Setup` contract does not validate whether the `endTime` is outdated. This oversight can lead to the contract utilizing an outdated report, potentially causing incorrect game finalization and financial discrepancies. Code snippet: ```solidity (int192 finalPrice, uint256 endTime) = IDataStreamsVerifier( ITreasury(treasury).upkeep() ).verifyReportWithTimestamp(unverifiedReport, data.feedNumber); ```

LowMissing Deposit Amount Validation in Game ContracFixed

In the `Bullseye` and `UpDown` contracts, `minDepositAmount` and `depositAmount` is set when starting a game. However, there is no validation to ensure that `minDepositAmount` and `depositAmount` matches the `minDepositAmount[token]` specified in the `Treasury` contract. This lack of validation can prevent players from joining the game if the deposit amount is incorrect.

LowMissing Zero Address ValidationFixed

The cited address input is missing a check that it is not `address(0)`.

LowPermit Function Doesn't Comply with DAI TokenFixed

Several contracts implement a feature allowing users to grant token allowances through a permit signature. However, there is a compatibility issue with the `DAI` stablecoin's unique permit function signature. The `DAI` contract at address `0x6B175474E89094C44Da98b954EedeAC495271d0F` defines its permit function with the following parameters: `function permit(address holder, address spender, uint256 nonce, uint256 expiry, bool allowed, uint8 v, bytes32 r, bytes32 s)`. This signature differs from the expected format, resulting in failed transactions because the contract's parameters do not align with those required by DAI's permit function. Consequently, any attempt to execute permit transactions with the incorrect parameters will inevitably fail.

LowPotential Game ID Collision RiskFixed

In the provided code snippets, game IDs are generated using the `keccak256` hash function with parameters such as `block.timestamp`, `endTime`, `msg.sender`, and other variables. The concern arises when multiple instances of the same game contract are deployed, and they share a common treasury contract. This can lead to potential game ID collisions. OneVsOneExactPrice Contract: ```solidity bytes32 gameId = keccak256( abi.encodePacked(endTime, block.timestamp, msg.sender, opponent) ); ``` Setup Contract: ```solidity bytes32 gameId = keccak256( abi.encodePacked( block.timestamp, endTime, takeProfitPrice, stopLossPrice, msg.sender ) ); ``` If the same parameters are used across different game contracts, the generated game IDs might be identical. When these IDs are used in a shared treasury contract, they could interfere with each other, potentially leading to asset loss and other unintended side effects.

LowPotential Insufficient Time Gap Between `stopPredFixed

The `startGame` function allows the creation of a game with specified parameters including `endTime` and `stopPredictAt`. Currently, it only checks if `endTime` is greater than `stopPredictAt`: `UpDown.sol` ```solidity=74 require(endTime > stopPredictAt, "Ending time must be higher"); ``` or `endTime` is greater than `block.timestamp`: `Bullseye.sol` ```solidity=91 require(endTime > block.timestamp, "Wrong ending time"); ``` This condition is insufficient as it does not enforce a significant time gap between `stopPredictAt` and `endTime`. If the gap is too small, players who join later have an advantage due to the predictability of price changes over short periods. This could lead to an unfair game environment.

LowPrice Storage Size and Price Precision IssuesFixed

In all games, the price precision used is 4 decimals. This is suitable for higher-priced tokens. However, for lower-priced tokens, this design doesn't effectively capture the price changes of the token. For example, for a token priced at \$1000, a price change of 0.00001%(\$0.0001) would be considered a price change in the game. However, for a token priced at \$0.01, a price change of 1%(\$0.0001) would need to occur before it is recognized as a price change. At the same time, using `uint32` to store 4-decimal prices allows for a maximum token price of around \$420,000. This will not be scalable in the future when token prices increase, and it also prevents the inclusion of some high-priced tokens, such as high-value LP tokens from Uniswap V2.

LowRate Issues in BullseyeFixed

According to the official documentation [xyro.io](https://xyro.io/onboarding/bulls-eye), when the number of participants is between 2 and 5, if the price is hit exactly, the first place receives 100%, otherwise, the first place gets 90% and the second place receives 10%. However, the code does not differentiate this situation and simply awards 100% of the rate bonus to the first place: `Bullseye.sol` ```solidity=14 uint256[3][5] public rates = [ [10000, 0, 0], [7500, 2500, 0], [9000, 1000, 0], [5000, 3500, 1500], [7500, 1500, 1000] ]; ``` `Bullseye.sol` ```solidity=306 if (packedGuessData.length <= 5) { ITreasury(treasury).withdrawGameFee( totalDeposited - depositAmount, fee, currentGameId ); currentRates = rates[0]; } else if (packedGuessData.length <= 10) { ITreasury(treasury).withdrawGameFee( totalDeposited - 2 * depositAmount, fee, currentGameId ); currentRates = closestDiff[0] <= exactRange ? rates[2] : rates[1]; } else { ITreasury(treasury).withdrawGameFee( totalDeposited - 3 * depositAmount, fee, currentGameId ); currentRates = closestDiff[0] <= exactRange ? rates[4] : rates[3]; } ``` There is an error in the official documentation regarding the distribution for exact price hits when there are 10+ players. It should align with the contract: 1st place - 75%, 2nd place - 15%, 3rd place - 10%. Additionally, the current `getRateIndex` function returns an index with a maximum value of 5, but the largest index in the `rates` array is 4: `Bullseye.sol` ```solidity=453 function getRateIndex( uint256 playersCount, bool isExact ) public pure returns (uint256 index) { if (playersCount <= 5) { index = isExact ? 1 : 0; } else if (playersCount <= 10) { index = isExact ? 3 : 2; } else { index = isExact ? 5 : 4; } } function setRate( uint256[3] memory rate, uint256 playersCount, bool isExact ) public onlyRole(DEFAULT_ADMIN_ROLE) { rates[getRateIndex(playersCount, isExact)] = rate; } } ```

LowSolidity version 0.8.20 may not work on other chaFixed

The compiler for Solidity 0.8.20 switches the default target EVM version to [Shanghai](https://blog.soliditylang.org/2023/05/10/solidity-0.8.20-release-announcement/#important-note), which includes the new `PUSH0` op code. This op code may not yet be implemented on all L2s, so deployment on these chains will fail. To work around this issue, use an earlier [EVM](https://docs.soliditylang.org/en/v0.8.20/using-the-compiler.html?ref=zaryabs.com#setting-the-evm-version-to-target) [version](https://book.getfoundry.sh/reference/config/solidity-compiler#evm_version)

Show 4 more
LowSolidity Version 0.8.23 Won't Work For All ChainsFixed

Since Solidity Release 0.8.23, `MCOPY` opcode is introduced. However, this may not be compatible with all chains and L2s. As a result, the compatibility of the code could be affected.

LowThe unvalidated `stopPredictAt` and `feedNumber`Fixed

In `startGame()`, there is no validation to check if `stopPredictAt` is greater than the current `block.timestamp`, and `feedNumber` is not validated to ensure it is a valid `feedNumber` capable of producing a price.

LowUnprotected Upgradeable ContractFixed

The `Treasury` logic contract does not protect the initializer. An attacker can front-run the `initialize` call and assume ownership of the logic contract. Once in control, the attacker can perform privileged operations, misleading users into believing that they are interacting with the legitimate owner of the upgradeable contract.

LowWeak Game State Check in `closeGame` of `Setup` GFixed

In `setUp`, as long as `block.timestamp > data.endTime`, the game master can call the `closeGame()` function: `Setup.sol` ```solidity=355 function closeGame(bytes32 gameId) public onlyRole(GAME_MASTER_ROLE) { GameInfo memory data = decodeData(gameId); require(data.startTime != 0, "Game doesn't exist"); require( ((data.startTime + (data.endTime - data.startTime) / 3 < block.timestamp && (data.SLplayers == 0 || data.TPplayers == 0)) || block.timestamp > data.endTime), "Wrong status!" ); //rewrites status games[gameId].packedData2 = (games[gameId].packedData2 & ~(uint256(0xFF) << 72)) | (uint256(uint8(Status.Cancelled)) << 72); emit SetupCancelled(gameId, data.initiator); } ``` When the game master mistakenly calls this function, changing the game’s status from "finished" to "cancelled", it will allow the losing player to refund their principal from the game, while preventing the winning player from receiving the reward. Only the principal can be refunded to winning player.

Contracts in scope

No deployed address is recorded for this audit.

The report names a repository or files. Open the report to see its scope.

Repository and files the report names (15)

  • https://github.com/xyro-io/smart-contracts/contracts/Setup.sol at commit 0a08cfae3ad04994241553312aa406350ebb3c73
  • https://github.com/xyro-io/smart-contracts/contracts/Treasury.sol at commit 740df06eed24a04f0e739c7c6332466dc6bae295
  • https://github.com/xyro-io/smart-contracts/contracts/Treasury.sol at commit 0a08cfae3ad04994241553312aa406350ebb3c73
  • https://github.com/xyro-io/smart-contracts/contracts/UpDown.sol at commit 0a08cfae3ad04994241553312aa406350ebb3c73
  • https://github.com/xyro-io/smart-contracts/contracts/OneVsOneExactPrice.sol at commit c72c06a3c1ceb178d0c9c887fcb9afcb04e44a4e
  • https://github.com/xyro-io/smart-contracts/contracts/UpDown.sol at commit c72c06a3c1ceb178d0c9c887fcb9afcb04e44a4e
  • https://github.com/xyro-io/smart-contracts/contracts/Bullseye.sol at commit 0a08cfae3ad04994241553312aa406350ebb3c73
  • https://github.com/xyro-io/smart-contracts/contracts/Setup.sol at commit 740df06eed24a04f0e739c7c6332466dc6bae295
  • https://github.com/xyro-io/smart-contracts/contracts/Bullseye.sol at commit 740df06eed24a04f0e739c7c6332466dc6bae295
  • https://github.com/xyro-io/smart-contracts/contracts/OneVsOneExactPrice.sol at commit 740df06eed24a04f0e739c7c6332466dc6bae295
  • https://github.com/xyro-io/smart-contracts/contracts/Setup.sol at commit c72c06a3c1ceb178d0c9c887fcb9afcb04e44a4e
  • https://github.com/xyro-io/smart-contracts/contracts/UpDown.sol at commit 740df06eed24a04f0e739c7c6332466dc6bae295
  • https://github.com/xyro-io/smart-contracts/contracts/Bullseye.sol at commit c72c06a3c1ceb178d0c9c887fcb9afcb04e44a4e
  • https://github.com/xyro-io/smart-contracts/contracts/OneVsOneExactPrice.sol at commit 0a08cfae3ad04994241553312aa406350ebb3c73
  • https://github.com/xyro-io/smart-contracts/contracts/Treasury.sol at commit c72c06a3c1ceb178d0c9c887fcb9afcb04e44a4e

Part of XYRO’s security record

XYRO
Audits4 audits by 2 firmsLast audit Jan 2025, by CertiK.
Critical findings1 critical acknowledged, not fixedReported in “Xyro” by Zokyo, Aug 2024. The team acknowledged it.
Open XYRO’s security record

Security firm

CertiK

1,044 projects reviewed · 1,375 reports · latest May 2025

See CertiK’s audits

Something wrong on this page? A missing report, a wrong date, a finding that was fixed since. Tell us and we check it against its source.

Tell us